Fossil SCM
Put the Content-Security-Policy in the HTTP reply header in addition to the HTML header. That way, the CSP is enforced even for raw HTML pages or if the skin provides an HTML header that omits the CSP. Add a new "default-csp" setting included with the skin that allows an administrator to change the CSP to allow for CDNs and such.
Commit
14c81d9d2b259d343cdb1ebd6d9f316b8abe4f0aed2add1213cd4b68b264d53f
Parent
6c0dfc8cc51e23a…
8 files changed
+6
+1
+20
+1
+1
+32
-43
+49
-5
+18
+6
| --- src/cgi.c | ||
| +++ src/cgi.c | ||
| @@ -209,10 +209,16 @@ | ||
| 209 | 209 | /* |
| 210 | 210 | ** Append text to the header of an HTTP reply |
| 211 | 211 | */ |
| 212 | 212 | void cgi_append_header(const char *zLine){ |
| 213 | 213 | blob_append(&extraHeader, zLine, -1); |
| 214 | +} | |
| 215 | +void cgi_printf_header(const char *zLine, ...){ | |
| 216 | + va_list ap; | |
| 217 | + va_start(ap, zLine); | |
| 218 | + blob_vappendf(&extraHeader, zLine, ap); | |
| 219 | + va_end(ap); | |
| 214 | 220 | } |
| 215 | 221 | |
| 216 | 222 | /* |
| 217 | 223 | ** Set a cookie by queuing up the appropriate HTTP header output. If |
| 218 | 224 | ** !g.isHTTP, this is a no-op. |
| 219 | 225 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -209,10 +209,16 @@ | |
| 209 | /* |
| 210 | ** Append text to the header of an HTTP reply |
| 211 | */ |
| 212 | void cgi_append_header(const char *zLine){ |
| 213 | blob_append(&extraHeader, zLine, -1); |
| 214 | } |
| 215 | |
| 216 | /* |
| 217 | ** Set a cookie by queuing up the appropriate HTTP header output. If |
| 218 | ** !g.isHTTP, this is a no-op. |
| 219 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -209,10 +209,16 @@ | |
| 209 | /* |
| 210 | ** Append text to the header of an HTTP reply |
| 211 | */ |
| 212 | void cgi_append_header(const char *zLine){ |
| 213 | blob_append(&extraHeader, zLine, -1); |
| 214 | } |
| 215 | void cgi_printf_header(const char *zLine, ...){ |
| 216 | va_list ap; |
| 217 | va_start(ap, zLine); |
| 218 | blob_vappendf(&extraHeader, zLine, ap); |
| 219 | va_end(ap); |
| 220 | } |
| 221 | |
| 222 | /* |
| 223 | ** Set a cookie by queuing up the appropriate HTTP header output. If |
| 224 | ** !g.isHTTP, this is a no-op. |
| 225 |
+1
| --- src/configure.c | ||
| +++ src/configure.c | ||
| @@ -107,10 +107,11 @@ | ||
| 107 | 107 | { "timeline-tslink-info", CONFIGSET_SKIN }, |
| 108 | 108 | { "timeline-utc", CONFIGSET_SKIN }, |
| 109 | 109 | { "adunit", CONFIGSET_SKIN }, |
| 110 | 110 | { "adunit-omit-if-admin", CONFIGSET_SKIN }, |
| 111 | 111 | { "adunit-omit-if-user", CONFIGSET_SKIN }, |
| 112 | + { "default-csp", CONFIGSET_SKIN }, | |
| 112 | 113 | { "sitemap-docidx", CONFIGSET_SKIN }, |
| 113 | 114 | { "sitemap-download", CONFIGSET_SKIN }, |
| 114 | 115 | { "sitemap-license", CONFIGSET_SKIN }, |
| 115 | 116 | { "sitemap-contact", CONFIGSET_SKIN }, |
| 116 | 117 | |
| 117 | 118 |
| --- src/configure.c | |
| +++ src/configure.c | |
| @@ -107,10 +107,11 @@ | |
| 107 | { "timeline-tslink-info", CONFIGSET_SKIN }, |
| 108 | { "timeline-utc", CONFIGSET_SKIN }, |
| 109 | { "adunit", CONFIGSET_SKIN }, |
| 110 | { "adunit-omit-if-admin", CONFIGSET_SKIN }, |
| 111 | { "adunit-omit-if-user", CONFIGSET_SKIN }, |
| 112 | { "sitemap-docidx", CONFIGSET_SKIN }, |
| 113 | { "sitemap-download", CONFIGSET_SKIN }, |
| 114 | { "sitemap-license", CONFIGSET_SKIN }, |
| 115 | { "sitemap-contact", CONFIGSET_SKIN }, |
| 116 | |
| 117 |
| --- src/configure.c | |
| +++ src/configure.c | |
| @@ -107,10 +107,11 @@ | |
| 107 | { "timeline-tslink-info", CONFIGSET_SKIN }, |
| 108 | { "timeline-utc", CONFIGSET_SKIN }, |
| 109 | { "adunit", CONFIGSET_SKIN }, |
| 110 | { "adunit-omit-if-admin", CONFIGSET_SKIN }, |
| 111 | { "adunit-omit-if-user", CONFIGSET_SKIN }, |
| 112 | { "default-csp", CONFIGSET_SKIN }, |
| 113 | { "sitemap-docidx", CONFIGSET_SKIN }, |
| 114 | { "sitemap-download", CONFIGSET_SKIN }, |
| 115 | { "sitemap-license", CONFIGSET_SKIN }, |
| 116 | { "sitemap-contact", CONFIGSET_SKIN }, |
| 117 | |
| 118 |
M
src/db.c
+20
| --- src/db.c | ||
| +++ src/db.c | ||
| @@ -3620,10 +3620,30 @@ | ||
| 3620 | 3620 | ** SETTING: th1-uri-regexp width=40 block-text |
| 3621 | 3621 | ** Specify which URI's are allowed in HTTP requests from |
| 3622 | 3622 | ** TH1 scripts. If empty, no HTTP requests are allowed |
| 3623 | 3623 | ** whatsoever. |
| 3624 | 3624 | */ |
| 3625 | +/* | |
| 3626 | +** SETTING: default-csp width=40 block-text | |
| 3627 | +** | |
| 3628 | +** The text of the Content Security Policy that is included | |
| 3629 | +** in the Content-Security-Policy: header field of the HTTP | |
| 3630 | +** reply and in the default HTML <head> section that is added when the | |
| 3631 | +** skin header does not specify a <head> section. The text "$nonce" | |
| 3632 | +** is replaced by the random nonce that is created for each web page. | |
| 3633 | +** | |
| 3634 | +** If this setting is an empty string or is omitted, then | |
| 3635 | +** the following default Content Security Policy is used: | |
| 3636 | +** | |
| 3637 | +** default-src 'self' data:; | |
| 3638 | +** script-src 'self' 'nonce-$nonce'; | |
| 3639 | +** style-src 'self' 'unsafe-inline'; | |
| 3640 | +** | |
| 3641 | +** The default CSP is recommended. The main reason to change | |
| 3642 | +** this setting would be to add CDNs from which it is safe to | |
| 3643 | +** load additional content. | |
| 3644 | +*/ | |
| 3625 | 3645 | /* |
| 3626 | 3646 | ** SETTING: uv-sync boolean default=off |
| 3627 | 3647 | ** If true, automatically send unversioned files as part |
| 3628 | 3648 | ** of a "fossil clone" or "fossil sync" command. The |
| 3629 | 3649 | ** default is false, in which case the -u option is |
| 3630 | 3650 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -3620,10 +3620,30 @@ | |
| 3620 | ** SETTING: th1-uri-regexp width=40 block-text |
| 3621 | ** Specify which URI's are allowed in HTTP requests from |
| 3622 | ** TH1 scripts. If empty, no HTTP requests are allowed |
| 3623 | ** whatsoever. |
| 3624 | */ |
| 3625 | /* |
| 3626 | ** SETTING: uv-sync boolean default=off |
| 3627 | ** If true, automatically send unversioned files as part |
| 3628 | ** of a "fossil clone" or "fossil sync" command. The |
| 3629 | ** default is false, in which case the -u option is |
| 3630 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -3620,10 +3620,30 @@ | |
| 3620 | ** SETTING: th1-uri-regexp width=40 block-text |
| 3621 | ** Specify which URI's are allowed in HTTP requests from |
| 3622 | ** TH1 scripts. If empty, no HTTP requests are allowed |
| 3623 | ** whatsoever. |
| 3624 | */ |
| 3625 | /* |
| 3626 | ** SETTING: default-csp width=40 block-text |
| 3627 | ** |
| 3628 | ** The text of the Content Security Policy that is included |
| 3629 | ** in the Content-Security-Policy: header field of the HTTP |
| 3630 | ** reply and in the default HTML <head> section that is added when the |
| 3631 | ** skin header does not specify a <head> section. The text "$nonce" |
| 3632 | ** is replaced by the random nonce that is created for each web page. |
| 3633 | ** |
| 3634 | ** If this setting is an empty string or is omitted, then |
| 3635 | ** the following default Content Security Policy is used: |
| 3636 | ** |
| 3637 | ** default-src 'self' data:; |
| 3638 | ** script-src 'self' 'nonce-$nonce'; |
| 3639 | ** style-src 'self' 'unsafe-inline'; |
| 3640 | ** |
| 3641 | ** The default CSP is recommended. The main reason to change |
| 3642 | ** this setting would be to add CDNs from which it is safe to |
| 3643 | ** load additional content. |
| 3644 | */ |
| 3645 | /* |
| 3646 | ** SETTING: uv-sync boolean default=off |
| 3647 | ** If true, automatically send unversioned files as part |
| 3648 | ** of a "fossil clone" or "fossil sync" command. The |
| 3649 | ** default is false, in which case the -u option is |
| 3650 |
+1
| --- src/doc.c | ||
| +++ src/doc.c | ||
| @@ -796,10 +796,11 @@ | ||
| 796 | 796 | if( !raw ){ |
| 797 | 797 | style_footer(); |
| 798 | 798 | } |
| 799 | 799 | #endif |
| 800 | 800 | }else{ |
| 801 | + fossil_free(style_csp(1)); | |
| 801 | 802 | cgi_set_content_type(zMime); |
| 802 | 803 | cgi_set_content(pBody); |
| 803 | 804 | } |
| 804 | 805 | } |
| 805 | 806 | |
| 806 | 807 |
| --- src/doc.c | |
| +++ src/doc.c | |
| @@ -796,10 +796,11 @@ | |
| 796 | if( !raw ){ |
| 797 | style_footer(); |
| 798 | } |
| 799 | #endif |
| 800 | }else{ |
| 801 | cgi_set_content_type(zMime); |
| 802 | cgi_set_content(pBody); |
| 803 | } |
| 804 | } |
| 805 | |
| 806 |
| --- src/doc.c | |
| +++ src/doc.c | |
| @@ -796,10 +796,11 @@ | |
| 796 | if( !raw ){ |
| 797 | style_footer(); |
| 798 | } |
| 799 | #endif |
| 800 | }else{ |
| 801 | fossil_free(style_csp(1)); |
| 802 | cgi_set_content_type(zMime); |
| 803 | cgi_set_content(pBody); |
| 804 | } |
| 805 | } |
| 806 | |
| 807 |
+1
| --- src/info.c | ||
| +++ src/info.c | ||
| @@ -1882,10 +1882,11 @@ | ||
| 1882 | 1882 | } |
| 1883 | 1883 | if( zFName ) zMime = mimetype_from_name(zFName); |
| 1884 | 1884 | if( zMime==0 ) zMime = "application/x-fossil-artifact"; |
| 1885 | 1885 | } |
| 1886 | 1886 | content_get(rid, &content); |
| 1887 | + fossil_free(style_csp(1)); | |
| 1887 | 1888 | cgi_set_content_type(zMime); |
| 1888 | 1889 | cgi_set_content(&content); |
| 1889 | 1890 | } |
| 1890 | 1891 | |
| 1891 | 1892 | /* |
| 1892 | 1893 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -1882,10 +1882,11 @@ | |
| 1882 | } |
| 1883 | if( zFName ) zMime = mimetype_from_name(zFName); |
| 1884 | if( zMime==0 ) zMime = "application/x-fossil-artifact"; |
| 1885 | } |
| 1886 | content_get(rid, &content); |
| 1887 | cgi_set_content_type(zMime); |
| 1888 | cgi_set_content(&content); |
| 1889 | } |
| 1890 | |
| 1891 | /* |
| 1892 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -1882,10 +1882,11 @@ | |
| 1882 | } |
| 1883 | if( zFName ) zMime = mimetype_from_name(zFName); |
| 1884 | if( zMime==0 ) zMime = "application/x-fossil-artifact"; |
| 1885 | } |
| 1886 | content_get(rid, &content); |
| 1887 | fossil_free(style_csp(1)); |
| 1888 | cgi_set_content_type(zMime); |
| 1889 | cgi_set_content(&content); |
| 1890 | } |
| 1891 | |
| 1892 | /* |
| 1893 |
+32
-43
| --- src/security_audit.c | ||
| +++ src/security_audit.c | ||
| @@ -33,12 +33,12 @@ | ||
| 33 | 33 | } |
| 34 | 34 | return 0; |
| 35 | 35 | } |
| 36 | 36 | |
| 37 | 37 | /* |
| 38 | -** Extract the content-security-policy from the reply header. Parse it | |
| 39 | -** up into separate fields, and return a pointer to a null-terminated | |
| 38 | +** Parse the content-security-policy | |
| 39 | +** into separate fields, and return a pointer to a null-terminated | |
| 40 | 40 | ** array of pointers to strings, one entry for each field. Or return |
| 41 | 41 | ** a NULL pointer if no CSP could be located in the header. |
| 42 | 42 | ** |
| 43 | 43 | ** Memory to hold the returned array and of the strings is obtained from |
| 44 | 44 | ** a single memory allocation, which the caller should free to avoid a |
| @@ -45,55 +45,44 @@ | ||
| 45 | 45 | ** memory leak. |
| 46 | 46 | */ |
| 47 | 47 | static char **parse_content_security_policy(void){ |
| 48 | 48 | char **azCSP = 0; |
| 49 | 49 | int nCSP = 0; |
| 50 | - const char *zHeader; | |
| 51 | - const char *zAll; | |
| 50 | + char *zAll; | |
| 52 | 51 | char *zCopy; |
| 53 | 52 | int nAll = 0; |
| 54 | - int ii, jj, n, nx = 0; | |
| 53 | + int jj; | |
| 55 | 54 | int nSemi; |
| 56 | 55 | |
| 57 | - zHeader = cgi_header(); | |
| 58 | - if( zHeader==0 ) return 0; | |
| 59 | - for(ii=0; zHeader[ii]; ii+=n){ | |
| 60 | - n = html_token_length(zHeader+ii); | |
| 61 | - if( zHeader[ii]=='<' | |
| 62 | - && fossil_strnicmp(html_attribute(zHeader+ii,"http-equiv",&nx), | |
| 63 | - "Content-Security-Policy",23)==0 | |
| 64 | - && nx==23 | |
| 65 | - && (zAll = html_attribute(zHeader+ii,"content",&nAll))!=0 | |
| 66 | - ){ | |
| 67 | - for(jj=nSemi=0; jj<nAll; jj++){ if( zAll[jj]==';' ) nSemi++; } | |
| 68 | - azCSP = fossil_malloc( nAll+1 + (nSemi+2)*sizeof(char*) ); | |
| 69 | - zCopy = (char*)&azCSP[nSemi+2]; | |
| 70 | - memcpy(zCopy,zAll,nAll); | |
| 71 | - zCopy[nAll] = 0; | |
| 72 | - while( fossil_isspace(zCopy[0]) || zCopy[0]==';' ){ zCopy++; } | |
| 73 | - azCSP[0] = zCopy; | |
| 74 | - nCSP = 1; | |
| 75 | - for(jj=0; zCopy[jj]; jj++){ | |
| 76 | - if( zCopy[jj]==';' ){ | |
| 77 | - int k; | |
| 78 | - for(k=jj-1; k>0 && fossil_isspace(zCopy[k]); k--){ zCopy[k] = 0; } | |
| 79 | - zCopy[jj] = 0; | |
| 80 | - while( jj+1<nAll | |
| 81 | - && (fossil_isspace(zCopy[jj+1]) || zCopy[jj+1]==';') | |
| 82 | - ){ | |
| 83 | - jj++; | |
| 84 | - } | |
| 85 | - assert( nCSP<nSemi+1 ); | |
| 86 | - azCSP[nCSP++] = zCopy+jj; | |
| 87 | - } | |
| 88 | - } | |
| 89 | - assert( nCSP<=nSemi+2 ); | |
| 90 | - azCSP[nCSP] = 0; | |
| 91 | - return azCSP; | |
| 92 | - } | |
| 93 | - } | |
| 94 | - return 0; | |
| 56 | + zAll = style_csp(0); | |
| 57 | + nAll = (int)strlen(zAll); | |
| 58 | + for(jj=nSemi=0; jj<nAll; jj++){ if( zAll[jj]==';' ) nSemi++; } | |
| 59 | + azCSP = fossil_malloc( nAll+1+(nSemi+2)*sizeof(char*) ); | |
| 60 | + zCopy = (char*)&azCSP[nSemi+2]; | |
| 61 | + memcpy(zCopy,zAll,nAll); | |
| 62 | + zCopy[nAll] = 0; | |
| 63 | + while( fossil_isspace(zCopy[0]) || zCopy[0]==';' ){ zCopy++; } | |
| 64 | + azCSP[0] = zCopy; | |
| 65 | + nCSP = 1; | |
| 66 | + for(jj=0; zCopy[jj]; jj++){ | |
| 67 | + if( zCopy[jj]==';' ){ | |
| 68 | + int k; | |
| 69 | + for(k=jj-1; k>0 && fossil_isspace(zCopy[k]); k--){ zCopy[k] = 0; } | |
| 70 | + zCopy[jj] = 0; | |
| 71 | + while( jj+1<nAll | |
| 72 | + && (fossil_isspace(zCopy[jj+1]) || zCopy[jj+1]==';') | |
| 73 | + ){ | |
| 74 | + jj++; | |
| 75 | + } | |
| 76 | + assert( nCSP<nSemi+1 ); | |
| 77 | + azCSP[nCSP++] = zCopy+jj; | |
| 78 | + } | |
| 79 | + } | |
| 80 | + assert( nCSP<=nSemi+2 ); | |
| 81 | + azCSP[nCSP] = 0; | |
| 82 | + fossil_free(zAll); | |
| 83 | + return azCSP; | |
| 95 | 84 | } |
| 96 | 85 | |
| 97 | 86 | /* |
| 98 | 87 | ** WEBPAGE: secaudit0 |
| 99 | 88 | ** |
| 100 | 89 |
| --- src/security_audit.c | |
| +++ src/security_audit.c | |
| @@ -33,12 +33,12 @@ | |
| 33 | } |
| 34 | return 0; |
| 35 | } |
| 36 | |
| 37 | /* |
| 38 | ** Extract the content-security-policy from the reply header. Parse it |
| 39 | ** up into separate fields, and return a pointer to a null-terminated |
| 40 | ** array of pointers to strings, one entry for each field. Or return |
| 41 | ** a NULL pointer if no CSP could be located in the header. |
| 42 | ** |
| 43 | ** Memory to hold the returned array and of the strings is obtained from |
| 44 | ** a single memory allocation, which the caller should free to avoid a |
| @@ -45,55 +45,44 @@ | |
| 45 | ** memory leak. |
| 46 | */ |
| 47 | static char **parse_content_security_policy(void){ |
| 48 | char **azCSP = 0; |
| 49 | int nCSP = 0; |
| 50 | const char *zHeader; |
| 51 | const char *zAll; |
| 52 | char *zCopy; |
| 53 | int nAll = 0; |
| 54 | int ii, jj, n, nx = 0; |
| 55 | int nSemi; |
| 56 | |
| 57 | zHeader = cgi_header(); |
| 58 | if( zHeader==0 ) return 0; |
| 59 | for(ii=0; zHeader[ii]; ii+=n){ |
| 60 | n = html_token_length(zHeader+ii); |
| 61 | if( zHeader[ii]=='<' |
| 62 | && fossil_strnicmp(html_attribute(zHeader+ii,"http-equiv",&nx), |
| 63 | "Content-Security-Policy",23)==0 |
| 64 | && nx==23 |
| 65 | && (zAll = html_attribute(zHeader+ii,"content",&nAll))!=0 |
| 66 | ){ |
| 67 | for(jj=nSemi=0; jj<nAll; jj++){ if( zAll[jj]==';' ) nSemi++; } |
| 68 | azCSP = fossil_malloc( nAll+1 + (nSemi+2)*sizeof(char*) ); |
| 69 | zCopy = (char*)&azCSP[nSemi+2]; |
| 70 | memcpy(zCopy,zAll,nAll); |
| 71 | zCopy[nAll] = 0; |
| 72 | while( fossil_isspace(zCopy[0]) || zCopy[0]==';' ){ zCopy++; } |
| 73 | azCSP[0] = zCopy; |
| 74 | nCSP = 1; |
| 75 | for(jj=0; zCopy[jj]; jj++){ |
| 76 | if( zCopy[jj]==';' ){ |
| 77 | int k; |
| 78 | for(k=jj-1; k>0 && fossil_isspace(zCopy[k]); k--){ zCopy[k] = 0; } |
| 79 | zCopy[jj] = 0; |
| 80 | while( jj+1<nAll |
| 81 | && (fossil_isspace(zCopy[jj+1]) || zCopy[jj+1]==';') |
| 82 | ){ |
| 83 | jj++; |
| 84 | } |
| 85 | assert( nCSP<nSemi+1 ); |
| 86 | azCSP[nCSP++] = zCopy+jj; |
| 87 | } |
| 88 | } |
| 89 | assert( nCSP<=nSemi+2 ); |
| 90 | azCSP[nCSP] = 0; |
| 91 | return azCSP; |
| 92 | } |
| 93 | } |
| 94 | return 0; |
| 95 | } |
| 96 | |
| 97 | /* |
| 98 | ** WEBPAGE: secaudit0 |
| 99 | ** |
| 100 |
| --- src/security_audit.c | |
| +++ src/security_audit.c | |
| @@ -33,12 +33,12 @@ | |
| 33 | } |
| 34 | return 0; |
| 35 | } |
| 36 | |
| 37 | /* |
| 38 | ** Parse the content-security-policy |
| 39 | ** into separate fields, and return a pointer to a null-terminated |
| 40 | ** array of pointers to strings, one entry for each field. Or return |
| 41 | ** a NULL pointer if no CSP could be located in the header. |
| 42 | ** |
| 43 | ** Memory to hold the returned array and of the strings is obtained from |
| 44 | ** a single memory allocation, which the caller should free to avoid a |
| @@ -45,55 +45,44 @@ | |
| 45 | ** memory leak. |
| 46 | */ |
| 47 | static char **parse_content_security_policy(void){ |
| 48 | char **azCSP = 0; |
| 49 | int nCSP = 0; |
| 50 | char *zAll; |
| 51 | char *zCopy; |
| 52 | int nAll = 0; |
| 53 | int jj; |
| 54 | int nSemi; |
| 55 | |
| 56 | zAll = style_csp(0); |
| 57 | nAll = (int)strlen(zAll); |
| 58 | for(jj=nSemi=0; jj<nAll; jj++){ if( zAll[jj]==';' ) nSemi++; } |
| 59 | azCSP = fossil_malloc( nAll+1+(nSemi+2)*sizeof(char*) ); |
| 60 | zCopy = (char*)&azCSP[nSemi+2]; |
| 61 | memcpy(zCopy,zAll,nAll); |
| 62 | zCopy[nAll] = 0; |
| 63 | while( fossil_isspace(zCopy[0]) || zCopy[0]==';' ){ zCopy++; } |
| 64 | azCSP[0] = zCopy; |
| 65 | nCSP = 1; |
| 66 | for(jj=0; zCopy[jj]; jj++){ |
| 67 | if( zCopy[jj]==';' ){ |
| 68 | int k; |
| 69 | for(k=jj-1; k>0 && fossil_isspace(zCopy[k]); k--){ zCopy[k] = 0; } |
| 70 | zCopy[jj] = 0; |
| 71 | while( jj+1<nAll |
| 72 | && (fossil_isspace(zCopy[jj+1]) || zCopy[jj+1]==';') |
| 73 | ){ |
| 74 | jj++; |
| 75 | } |
| 76 | assert( nCSP<nSemi+1 ); |
| 77 | azCSP[nCSP++] = zCopy+jj; |
| 78 | } |
| 79 | } |
| 80 | assert( nCSP<=nSemi+2 ); |
| 81 | azCSP[nCSP] = 0; |
| 82 | fossil_free(zAll); |
| 83 | return azCSP; |
| 84 | } |
| 85 | |
| 86 | /* |
| 87 | ** WEBPAGE: secaudit0 |
| 88 | ** |
| 89 |
+49
-5
| --- src/style.c | ||
| +++ src/style.c | ||
| @@ -473,10 +473,55 @@ | ||
| 473 | 473 | sqlite3_randomness(24, zSeed); |
| 474 | 474 | encode16(zSeed,(unsigned char*)zNonce,24); |
| 475 | 475 | } |
| 476 | 476 | return zNonce; |
| 477 | 477 | } |
| 478 | + | |
| 479 | +/* | |
| 480 | +** Return the default Content Security Policy (CSP) string. | |
| 481 | +** If the toHeader argument is true, then also add the | |
| 482 | +** CSP to the HTTP reply header. | |
| 483 | +** | |
| 484 | +** The CSP comes from the "default-csp" setting if it exists and | |
| 485 | +** is non-empty. If that setting is an empty string, then the following | |
| 486 | +** default is used instead: | |
| 487 | +** | |
| 488 | +** default-src 'self' data:; | |
| 489 | +** script-src 'self' 'nonce-$nonce'; | |
| 490 | +** style-src 'self' 'unsafe-inline'; | |
| 491 | +** | |
| 492 | +** The text '$nonce' is replaced by style_nonce() if and whereever it | |
| 493 | +** occurs in the input string. | |
| 494 | +** | |
| 495 | +** The string returned is obtained from fossil_malloc() and | |
| 496 | +** should be released by the caller. | |
| 497 | +*/ | |
| 498 | +char *style_csp(int toHeader){ | |
| 499 | + static const char zBackupCSP[] = | |
| 500 | + "default-src 'self' data:; " | |
| 501 | + "script-src 'self' 'nonce-$nonce'; " | |
| 502 | + "style-src 'self' 'unsafe-inline'"; | |
| 503 | + const char *zFormat = db_get("default-csp",""); | |
| 504 | + Blob csp; | |
| 505 | + char *zNonce; | |
| 506 | + char *zCsp; | |
| 507 | + if( zFormat[0]==0 ){ | |
| 508 | + zFormat = zBackupCSP; | |
| 509 | + } | |
| 510 | + blob_init(&csp, 0, 0); | |
| 511 | + while( zFormat[0] && (zNonce = strstr(zFormat,"$nonce"))!=0 ){ | |
| 512 | + blob_append(&csp, zFormat, (int)(zNonce - zFormat)); | |
| 513 | + blob_append(&csp, style_nonce(), -1); | |
| 514 | + zFormat = zNonce + 6; | |
| 515 | + } | |
| 516 | + blob_append(&csp, zFormat, -1); | |
| 517 | + zCsp = blob_str(&csp); | |
| 518 | + if( toHeader ){ | |
| 519 | + cgi_printf_header("Content-Security-Policy: %s\r\n", zCsp); | |
| 520 | + } | |
| 521 | + return zCsp; | |
| 522 | +} | |
| 478 | 523 | |
| 479 | 524 | /* |
| 480 | 525 | ** Default HTML page header text through <body>. If the repository-specific |
| 481 | 526 | ** header template lacks a <body> tag, then all of the following is |
| 482 | 527 | ** prepended. |
| @@ -498,21 +543,20 @@ | ||
| 498 | 543 | /* |
| 499 | 544 | ** Initialize all the default TH1 variables |
| 500 | 545 | */ |
| 501 | 546 | static void style_init_th1_vars(const char *zTitle){ |
| 502 | 547 | const char *zNonce = style_nonce(); |
| 548 | + char *zDfltCsp; | |
| 549 | + | |
| 550 | + zDfltCsp = style_csp(1); | |
| 503 | 551 | /* |
| 504 | 552 | ** Do not overwrite the TH1 variable "default_csp" if it exists, as this |
| 505 | 553 | ** allows it to be properly overridden via the TH1 setup script (i.e. it |
| 506 | 554 | ** is evaluated before the header is rendered). |
| 507 | 555 | */ |
| 508 | - char *zDfltCsp = sqlite3_mprintf("default-src 'self' data: ; " | |
| 509 | - "script-src 'self' 'nonce-%s' ; " | |
| 510 | - "style-src 'self' 'unsafe-inline'", | |
| 511 | - zNonce); | |
| 512 | 556 | Th_MaybeStore("default_csp", zDfltCsp); |
| 513 | - sqlite3_free(zDfltCsp); | |
| 557 | + fossil_free(zDfltCsp); | |
| 514 | 558 | Th_Store("nonce", zNonce); |
| 515 | 559 | Th_Store("project_name", db_get("project-name","Unnamed Fossil Project")); |
| 516 | 560 | Th_Store("project_description", db_get("project-description","")); |
| 517 | 561 | if( zTitle ) Th_Store("title", zTitle); |
| 518 | 562 | Th_Store("baseurl", g.zBaseURL); |
| 519 | 563 | |
| 520 | 564 | ADDED test/csp1.html |
| --- src/style.c | |
| +++ src/style.c | |
| @@ -473,10 +473,55 @@ | |
| 473 | sqlite3_randomness(24, zSeed); |
| 474 | encode16(zSeed,(unsigned char*)zNonce,24); |
| 475 | } |
| 476 | return zNonce; |
| 477 | } |
| 478 | |
| 479 | /* |
| 480 | ** Default HTML page header text through <body>. If the repository-specific |
| 481 | ** header template lacks a <body> tag, then all of the following is |
| 482 | ** prepended. |
| @@ -498,21 +543,20 @@ | |
| 498 | /* |
| 499 | ** Initialize all the default TH1 variables |
| 500 | */ |
| 501 | static void style_init_th1_vars(const char *zTitle){ |
| 502 | const char *zNonce = style_nonce(); |
| 503 | /* |
| 504 | ** Do not overwrite the TH1 variable "default_csp" if it exists, as this |
| 505 | ** allows it to be properly overridden via the TH1 setup script (i.e. it |
| 506 | ** is evaluated before the header is rendered). |
| 507 | */ |
| 508 | char *zDfltCsp = sqlite3_mprintf("default-src 'self' data: ; " |
| 509 | "script-src 'self' 'nonce-%s' ; " |
| 510 | "style-src 'self' 'unsafe-inline'", |
| 511 | zNonce); |
| 512 | Th_MaybeStore("default_csp", zDfltCsp); |
| 513 | sqlite3_free(zDfltCsp); |
| 514 | Th_Store("nonce", zNonce); |
| 515 | Th_Store("project_name", db_get("project-name","Unnamed Fossil Project")); |
| 516 | Th_Store("project_description", db_get("project-description","")); |
| 517 | if( zTitle ) Th_Store("title", zTitle); |
| 518 | Th_Store("baseurl", g.zBaseURL); |
| 519 | |
| 520 | DDED test/csp1.html |
| --- src/style.c | |
| +++ src/style.c | |
| @@ -473,10 +473,55 @@ | |
| 473 | sqlite3_randomness(24, zSeed); |
| 474 | encode16(zSeed,(unsigned char*)zNonce,24); |
| 475 | } |
| 476 | return zNonce; |
| 477 | } |
| 478 | |
| 479 | /* |
| 480 | ** Return the default Content Security Policy (CSP) string. |
| 481 | ** If the toHeader argument is true, then also add the |
| 482 | ** CSP to the HTTP reply header. |
| 483 | ** |
| 484 | ** The CSP comes from the "default-csp" setting if it exists and |
| 485 | ** is non-empty. If that setting is an empty string, then the following |
| 486 | ** default is used instead: |
| 487 | ** |
| 488 | ** default-src 'self' data:; |
| 489 | ** script-src 'self' 'nonce-$nonce'; |
| 490 | ** style-src 'self' 'unsafe-inline'; |
| 491 | ** |
| 492 | ** The text '$nonce' is replaced by style_nonce() if and whereever it |
| 493 | ** occurs in the input string. |
| 494 | ** |
| 495 | ** The string returned is obtained from fossil_malloc() and |
| 496 | ** should be released by the caller. |
| 497 | */ |
| 498 | char *style_csp(int toHeader){ |
| 499 | static const char zBackupCSP[] = |
| 500 | "default-src 'self' data:; " |
| 501 | "script-src 'self' 'nonce-$nonce'; " |
| 502 | "style-src 'self' 'unsafe-inline'"; |
| 503 | const char *zFormat = db_get("default-csp",""); |
| 504 | Blob csp; |
| 505 | char *zNonce; |
| 506 | char *zCsp; |
| 507 | if( zFormat[0]==0 ){ |
| 508 | zFormat = zBackupCSP; |
| 509 | } |
| 510 | blob_init(&csp, 0, 0); |
| 511 | while( zFormat[0] && (zNonce = strstr(zFormat,"$nonce"))!=0 ){ |
| 512 | blob_append(&csp, zFormat, (int)(zNonce - zFormat)); |
| 513 | blob_append(&csp, style_nonce(), -1); |
| 514 | zFormat = zNonce + 6; |
| 515 | } |
| 516 | blob_append(&csp, zFormat, -1); |
| 517 | zCsp = blob_str(&csp); |
| 518 | if( toHeader ){ |
| 519 | cgi_printf_header("Content-Security-Policy: %s\r\n", zCsp); |
| 520 | } |
| 521 | return zCsp; |
| 522 | } |
| 523 | |
| 524 | /* |
| 525 | ** Default HTML page header text through <body>. If the repository-specific |
| 526 | ** header template lacks a <body> tag, then all of the following is |
| 527 | ** prepended. |
| @@ -498,21 +543,20 @@ | |
| 543 | /* |
| 544 | ** Initialize all the default TH1 variables |
| 545 | */ |
| 546 | static void style_init_th1_vars(const char *zTitle){ |
| 547 | const char *zNonce = style_nonce(); |
| 548 | char *zDfltCsp; |
| 549 | |
| 550 | zDfltCsp = style_csp(1); |
| 551 | /* |
| 552 | ** Do not overwrite the TH1 variable "default_csp" if it exists, as this |
| 553 | ** allows it to be properly overridden via the TH1 setup script (i.e. it |
| 554 | ** is evaluated before the header is rendered). |
| 555 | */ |
| 556 | Th_MaybeStore("default_csp", zDfltCsp); |
| 557 | fossil_free(zDfltCsp); |
| 558 | Th_Store("nonce", zNonce); |
| 559 | Th_Store("project_name", db_get("project-name","Unnamed Fossil Project")); |
| 560 | Th_Store("project_description", db_get("project-description","")); |
| 561 | if( zTitle ) Th_Store("title", zTitle); |
| 562 | Th_Store("baseurl", g.zBaseURL); |
| 563 | |
| 564 | DDED test/csp1.html |
+18
| --- a/test/csp1.html | ||
| +++ b/test/csp1.html | ||
| @@ -0,0 +1,18 @@ | ||
| 1 | +<!DOCTYPE html> | |
| 2 | +<html> | |
| 3 | +<head> | |
| 4 | +<title>Title: Content Security Policy Test</title> | |
| 5 | +</head> | |
| 6 | +<body> | |
| 7 | +<h1>Content Security Policy Test</h1> | |
| 8 | + | |
| 9 | +<p>If the content-security-policy is ineffective, a pop-up dialog | |
| 10 | +box will appears. If there is no dialog box, then CSP is working | |
| 11 | +correctly.</p> | |
| 12 | + | |
| 13 | +<script>alert('Content Security Policy is ineffective');</script> | |
| 14 | +<img src='/' onerror='alert("CSP is ineffective")'> | |
| 15 | + | |
| 16 | +<p>As a double-check, open the Developer Console in your web-browser | |
| 17 | +and verify that two CSP violations were detected and blocked.</p> | |
| 18 | +</body> |
| --- a/test/csp1.html | |
| +++ b/test/csp1.html | |
| @@ -0,0 +1,18 @@ | |
| --- a/test/csp1.html | |
| +++ b/test/csp1.html | |
| @@ -0,0 +1,18 @@ | |
| 1 | <!DOCTYPE html> |
| 2 | <html> |
| 3 | <head> |
| 4 | <title>Title: Content Security Policy Test</title> |
| 5 | </head> |
| 6 | <body> |
| 7 | <h1>Content Security Policy Test</h1> |
| 8 | |
| 9 | <p>If the content-security-policy is ineffective, a pop-up dialog |
| 10 | box will appears. If there is no dialog box, then CSP is working |
| 11 | correctly.</p> |
| 12 | |
| 13 | <script>alert('Content Security Policy is ineffective');</script> |
| 14 | <img src='/' onerror='alert("CSP is ineffective")'> |
| 15 | |
| 16 | <p>As a double-check, open the Developer Console in your web-browser |
| 17 | and verify that two CSP violations were detected and blocked.</p> |
| 18 | </body> |