Fossil

Check-in [9cf90a4f9d]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Have the security-audit page analyze and display the content security policy.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 9cf90a4f9d134b7fddb190e27c33c8d5d6f25fd25d37a2241e3fae02c715b5e9
User & Date: drh 2019-08-19 17:18:31.499
Context
2019-08-20
02:09
Fix a compiler warning in the security-audit page. ... (check-in: 3243a6c148 user: drh tags: trunk)
01:34
Added --with-sanitizer configure-time option for appending -fsanitize=VALUE to CFLAGS and LDFLAGS, plus automatic detection of -lubsan for GCC, which doesn't automatically link to that with -fsanitize=undefined as Clang does. EDIT: This check-in breaks the built on Ubuntu 18.04. ... (check-in: 7907b6ffae user: wyoung tags: configure-updates)
2019-08-19
17:18
Have the security-audit page analyze and display the content security policy. ... (check-in: 9cf90a4f9d user: drh tags: trunk)
13:04
Increase the default HTTP request timeout to 10 minutes. Provide the FOSSIL_DEFAULT_TIMEOUT compile-time option for setting an alternative default. ... (check-in: 7979989dff user: drh tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/cgi.c.
142
143
144
145
146
147
148







149
150
151
152
153
154
155

/*
** Return a pointer to the CGI output blob.
*/
Blob *cgi_output_blob(void){
  return pContent;
}








/*
** Combine the header and body of the CGI into a single string.
*/
static void cgi_combine_header_and_body(void){
  int size = blob_size(&cgiContent[1]);
  if( size>0 ){







>
>
>
>
>
>
>







142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162

/*
** Return a pointer to the CGI output blob.
*/
Blob *cgi_output_blob(void){
  return pContent;
}

/*
** Return complete text of the output header
*/
const char *cgi_header(void){
  return blob_str(&cgiContent[0]);
}

/*
** Combine the header and body of the CGI into a single string.
*/
static void cgi_combine_header_and_body(void){
  int size = blob_size(&cgiContent[1]);
  if( size>0 ){
Changes to src/security_audit.c.
30
31
32
33
34
35
36



























































37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

55
56
57
58
59
60
61
  while( zTest[0] ){
    if( strchr(zCap, zTest[0]) ) return 1;
    zTest++;
  }
  return 0;
}





























































/*
** WEBPAGE: secaudit0
**
** Run a security audit of the current Fossil setup, looking
** for configuration problems that might allow unauthorized
** access to the repository.
**
** This page requires administrator access.  It is usually
** accessed using the Admin/Security-Audit menu option
** from any of the default skins.
*/
void secaudit0_page(void){
  const char *zAnonCap;      /* Capabilities of user "anonymous" and "nobody" */
  const char *zPubPages;     /* GLOB pattern for public pages */
  const char *zSelfCap;      /* Capabilities of self-registered users */
  char *z;
  int n;


  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
  }
  style_header("Security Audit");







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


















>







30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
  while( zTest[0] ){
    if( strchr(zCap, zTest[0]) ) return 1;
    zTest++;
  }
  return 0;
}

/*
** Extract the content-security-policy from the reply header.  Parse it
** up into separate fields, and return a pointer to a null-terminated
** array of pointers to strings, one entry for each field.  Or return
** a NULL pointer if no CSP could be located in the header.
**
** Memory to hold the returned array and of the strings is obtained from
** a single memory allocation, which the caller should free to avoid a
** memory leak.
*/
static char **parse_content_security_policy(void){
  char **azCSP = 0;
  int nCSP = 0;
  const char *zHeader;
  const char *zAll;
  char *zCopy;
  int nAll = 0;
  int ii, jj, n, nx = 0;
  int nSemi;

  zHeader = cgi_header();
  if( zHeader==0 ) return 0;
  for(ii=0; zHeader[ii]; ii+=n){
    n = html_token_length(zHeader+ii);
    if( zHeader[ii]=='<'
     && fossil_strnicmp(html_attribute(zHeader+ii,"http-equiv",&nx),
                        "Content-Security-Policy",23)==0
     && nx==23
     && (zAll = html_attribute(zHeader+ii,"content",&nAll))!=0
    ){
      for(jj=nSemi=0; jj<nAll; jj++){ if( zAll[jj]==';' ) nSemi++; }
      azCSP = fossil_malloc( nAll+1 + (nSemi+2)*sizeof(char*) );
      zCopy = &azCSP[nSemi+2];
      memcpy(zCopy,zAll,nAll);
      zCopy[nAll] = 0;
      while( fossil_isspace(zCopy[0]) || zCopy[0]==';' ){ zCopy++; }
      azCSP[0] = zCopy;
      nCSP = 1;
      for(jj=0; zCopy[jj]; jj++){
        if( zCopy[jj]==';' ){
          int k;
          for(k=jj-1; k>0 && fossil_isspace(zCopy[k]); k--){ zCopy[k] = 0; }
          zCopy[jj] = 0;
          while( jj+1<nAll
             && (fossil_isspace(zCopy[jj+1]) || zCopy[jj+1]==';')
          ){
            jj++;
          }
          assert( nCSP<nSemi+1 );
          azCSP[nCSP++] = zCopy+jj;
        }
      }
      assert( nCSP<=nSemi+2 );
      azCSP[nCSP] = 0;
      return azCSP;
    }
  }
  return 0;
}

/*
** WEBPAGE: secaudit0
**
** Run a security audit of the current Fossil setup, looking
** for configuration problems that might allow unauthorized
** access to the repository.
**
** This page requires administrator access.  It is usually
** accessed using the Admin/Security-Audit menu option
** from any of the default skins.
*/
void secaudit0_page(void){
  const char *zAnonCap;      /* Capabilities of user "anonymous" and "nobody" */
  const char *zPubPages;     /* GLOB pattern for public pages */
  const char *zSelfCap;      /* Capabilities of self-registered users */
  char *z;
  int n;
  char **azCSP;              /* Parsed content security policy */

  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
  }
  style_header("Security Audit");
437
438
439
440
441
442
443




















444
445
446
447
448
449
450
    @ at <a href='%R/extfilelist'>%h(g.zExtRoot)</a> holding
    @ %d(nCgi) CGIs and %d(nFile-nCgi) static content and data files.
  }

  @ <li><p> User capability summary:
  capability_summary();





















  if( alert_enabled() ){
    @ <li><p> Email alert configuration summary:
    @ <table class="label-value">
    stats_for_email();
    @ </table>
  }else{
    @ <li><p> Email alerts are disabled







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
    @ at <a href='%R/extfilelist'>%h(g.zExtRoot)</a> holding
    @ %d(nCgi) CGIs and %d(nFile-nCgi) static content and data files.
  }

  @ <li><p> User capability summary:
  capability_summary();


  azCSP = parse_content_security_policy();
  if( azCSP==0 ){
    @ <li><p> WARNING: No Content Security Policy (CSP) is specified in the
    @ header. Though not required, a strong CSP is recommended. Fossil will
    @ automatically insert an appropriate CSP if you let it generate the
    @ HTML <tt>&lt;head&gt;</tt> element by omitting <tt>&lt;body&gt;</tt>
    @ from the header configuration in your customized skin.
    @ 
  }else{
    int ii;
    @ <li><p> Content Security Policy:
    @ <ol type="a">
    for(ii=0; azCSP[ii]; ii++){
      @ <li>%h(azCSP[ii])
    }
    @ </ol>
  }
  fossil_free(azCSP);

  if( alert_enabled() ){
    @ <li><p> Email alert configuration summary:
    @ <table class="label-value">
    stats_for_email();
    @ </table>
  }else{
    @ <li><p> Email alerts are disabled
Changes to src/wikiformat.c.
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044






































































































2045
2046
2047
2048
2049
2050
2051
    }
    z += n;
  }
  free(renderer.aStack);
}

/*
** Get the next HTML token.
**
** z points to the start of a token.  Return the number of
** characters in that token.
*/
static int nextHtmlToken(const char *z){
  int n;
  char c;
  if( (c=z[0])=='<' ){
    n = htmlTagLength(z);
    if( n<=0 ) n = 1;
  }else if( fossil_isspace(c) ){
    for(n=1; z[n] && fossil_isspace(z[n]); n++){}
  }else if( c=='&' ){
    n = z[1]=='#' ? 2 : 1;
    while( fossil_isalnum(z[n]) ) n++;
    if( z[n]==';' ) n++;
  }else{
    n = 1;
    for(n=1; 1; n++){
      if( (c = z[n]) > '<' ) continue;
      if( c=='<' || c=='&' || fossil_isspace(c) || c==0 ) break;
    }
  }
  return n;
}







































































































/*
** Attempt to reformat messy HTML to be easily readable by humans.
**
**    *  Try to keep lines less than 80 characters in length
**    *  Collapse white space into a single space
**    *  Put a blank line before:







|
<
<
<

|




















>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







2012
2013
2014
2015
2016
2017
2018
2019



2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
    }
    z += n;
  }
  free(renderer.aStack);
}

/*
** Return the length, in bytes, of the HTML token that z is pointing to.



*/
int html_token_length(const char *z){
  int n;
  char c;
  if( (c=z[0])=='<' ){
    n = htmlTagLength(z);
    if( n<=0 ) n = 1;
  }else if( fossil_isspace(c) ){
    for(n=1; z[n] && fossil_isspace(z[n]); n++){}
  }else if( c=='&' ){
    n = z[1]=='#' ? 2 : 1;
    while( fossil_isalnum(z[n]) ) n++;
    if( z[n]==';' ) n++;
  }else{
    n = 1;
    for(n=1; 1; n++){
      if( (c = z[n]) > '<' ) continue;
      if( c=='<' || c=='&' || fossil_isspace(c) || c==0 ) break;
    }
  }
  return n;
}

/*
** z points to someplace in the middle of HTML markup.  Return the length
** of the subtoken that starts on z.
*/
int html_subtoken_length(const char *z){
  int n;
  char c;
  c = z[0];
  if( fossil_isspace(c) ){
    for(n=1; z[n] && fossil_isspace(z[n]); n++){}
    return n;
  }
  if( c=='"' || c=='\'' ){
    for(n=1; z[n] && z[n]!=c && z[n]!='>'; n++){}
    if( z[n]==c ) n++;
    return n;
  }
  if( c=='>' ){
    return 0;
  }
  if( c=='=' ){
    return 1;
  }
  if( fossil_isalnum(c) || c=='/' ){
    for(n=1; (c=z[n])!=0 && (fossil_isalnum(c) || c=='-' || c=='_'); n++){}
    return n;
  }
  return 1;
}

/*
** z points to an HTML markup token:  <TAG ATTR=VALUE ...>
** This routine looks for the VALUE associated with zAttr and returns
** a pointer to the start of that value and sets *pLen to be the length
** in bytes for the value.  Or it returns NULL if no such attr exists.
*/
const char *html_attribute(const char *zMarkup, const char *zAttr, int *pLen){
  int i = 1;
  int n;
  int nAttr;
  int iMatchCnt = 0;
  assert( zMarkup[0]=='<' );
  assert( zMarkup[1]!=0 );
  n = html_subtoken_length(zMarkup+i);
  if( n==0 ) return 0;
  i += n;
  nAttr = (int)strlen(zAttr);
  while( 1 ){
    const char *zStart = zMarkup+i;
    n = html_subtoken_length(zStart);
    if( n==0 ) break;
    i += n;
    if( fossil_isspace(zStart[0]) ) continue;
    if( n==nAttr && fossil_strnicmp(zAttr,zStart,nAttr)==0 ){
      iMatchCnt = 1;
    }else if( n==1 && zStart[0]=='=' && iMatchCnt==1 ){
      iMatchCnt = 2;
    }else if( iMatchCnt==2 ){
      if( (zStart[0]=='"' || zStart[0]=='\'') && zStart[n-1]==zStart[0] ){
        zStart++;
        n -= 2;
      } 
      *pLen = n;
      return zStart;
    }else{
      iMatchCnt = 0;
    }
  }
  return 0;
}

/*
** COMMAND: test-html-tokenize
**
** Tokenize an HTML file.  Return the offset and length and text of
** each token - one token per line.  Omit white-space tokens.
*/
void test_html_tokenize(void){
  Blob in;
  char *z;
  int i;
  int iOfst, n;

  for(i=2; i<g.argc; i++){
    blob_read_from_file(&in, g.argv[i], ExtFILE);
    z = blob_str(&in);
    for(iOfst=0; z[iOfst]; iOfst+=n){
      n = html_token_length(z+iOfst);
      if( fossil_isspace(z[iOfst]) ) continue;
      fossil_print("%d %d %.*s\n", iOfst, n, n, z+iOfst);
      if( z[iOfst]=='<' && n>1 ){
        int j,k;
        for(j=iOfst+1; (k = html_subtoken_length(z+j))>0; j+=k){
          if( fossil_isspace(z[j]) || z[j]=='=' ) continue;
          fossil_print("# %d %d %.*s\n", j, k, k, z+j);
        }
      }
    }
    blob_reset(&in);
  }
}

/*
** Attempt to reformat messy HTML to be easily readable by humans.
**
**    *  Try to keep lines less than 80 characters in length
**    *  Collapse white space into a single space
**    *  Put a blank line before:
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
void htmlTidy(const char *zIn, Blob *pOut){
  int n;
  int nPre = 0;
  int iCur = 0;
  int wantSpace = 0;
  int omitSpace = 1;
  while( zIn[0] ){
    n = nextHtmlToken(zIn);
    if( zIn[0]=='<' && n>1 ){
      int i, j;
      int isCloseTag;
      int eTag;
      int eType;
      char zTag[32];
      isCloseTag = zIn[1]=='/';







|







2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
void htmlTidy(const char *zIn, Blob *pOut){
  int n;
  int nPre = 0;
  int iCur = 0;
  int wantSpace = 0;
  int omitSpace = 1;
  while( zIn[0] ){
    n = html_token_length(zIn);
    if( zIn[0]=='<' && n>1 ){
      int i, j;
      int isCloseTag;
      int eTag;
      int eType;
      char zTag[32];
      isCloseTag = zIn[1]=='/';
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
  int i, j;
  int inTitle = 0;          /* True between <title>...</title> */
  int seenText = 0;         /* True after first non-whitespace seen */
  int nNL = 0;              /* Number of \n characters at the end of pOut */
  int nWS = 0;              /* True if pOut ends with whitespace */
  while( fossil_isspace(zIn[0]) ) zIn++;
  while( zIn[0] ){
    n = nextHtmlToken(zIn);
    if( zIn[0]=='<' && n>1 ){
      int isCloseTag;
      int eTag;
      int eType;
      char zTag[32];
      isCloseTag = zIn[1]=='/';
      for(i=0, j=1+isCloseTag; i<30 && fossil_isalnum(zIn[j]); i++, j++){
         zTag[i] = fossil_tolower(zIn[j]);
      }
      zTag[i] = 0;
      eTag = findTag(zTag);
      eType = aMarkup[eTag].iType;
      if( eTag==MARKUP_INVALID && fossil_strnicmp(zIn,"<style",6)==0 ){
        zIn += n;
        while( zIn[0] ){
          n = nextHtmlToken(zIn);
          if( fossil_strnicmp(zIn, "</style",7)==0 ) break;
          zIn += n;
        }
        if( zIn[0]=='<' ) zIn += n;
        continue;
      }
      if( eTag==MARKUP_TITLE ){







|















|







2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
  int i, j;
  int inTitle = 0;          /* True between <title>...</title> */
  int seenText = 0;         /* True after first non-whitespace seen */
  int nNL = 0;              /* Number of \n characters at the end of pOut */
  int nWS = 0;              /* True if pOut ends with whitespace */
  while( fossil_isspace(zIn[0]) ) zIn++;
  while( zIn[0] ){
    n = html_token_length(zIn);
    if( zIn[0]=='<' && n>1 ){
      int isCloseTag;
      int eTag;
      int eType;
      char zTag[32];
      isCloseTag = zIn[1]=='/';
      for(i=0, j=1+isCloseTag; i<30 && fossil_isalnum(zIn[j]); i++, j++){
         zTag[i] = fossil_tolower(zIn[j]);
      }
      zTag[i] = 0;
      eTag = findTag(zTag);
      eType = aMarkup[eTag].iType;
      if( eTag==MARKUP_INVALID && fossil_strnicmp(zIn,"<style",6)==0 ){
        zIn += n;
        while( zIn[0] ){
          n = html_token_length(zIn);
          if( fossil_strnicmp(zIn, "</style",7)==0 ) break;
          zIn += n;
        }
        if( zIn[0]=='<' ) zIn += n;
        continue;
      }
      if( eTag==MARKUP_TITLE ){