Fossil

Check-in [295b814a27]
Login

Check-in [295b814a27]

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

Overview
Comment:New setting "vuln-report" determines what to do when tainted text is misused in a TH1 script. Enhance the /test-warning page to deliberately misuse tainted text in TH1 to verify error handling. Enhance /errorlog to separate out TH1 vulnerability reports as a new category the the error log.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | th1-taint
Files: files | file ages | folders
SHA3-256: 295b814a2722fff73e65c10b5247fbec4cbcf7178a706621eb743efa98bf30ff
User & Date: drh 2025-04-20 16:13:27.386
Context
2025-04-20
16:54
Add "taint mode" to TH1. Attempts to output values that are derived from user input as unescaped HTML, or to use such values unescaped in SQL, raises errors. The resolution of these errors depends on the value of the new "vuln-report" setting. ... (check-in: 2116238e80 user: drh tags: trunk)
16:13
New setting "vuln-report" determines what to do when tainted text is misused in a TH1 script. Enhance the /test-warning page to deliberately misuse tainted text in TH1 to verify error handling. Enhance /errorlog to separate out TH1 vulnerability reports as a new category the the error log. ... (Closed-Leaf check-in: 295b814a27 user: drh tags: th1-taint)
2025-04-19
23:32
Fix more issues that were already fixed but overwritten by text editor errors and didn't get committed last time. ... (check-in: bd45dc72dd user: drh tags: th1-taint)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/main.c.
3716
3717
3718
3719
3720
3721
3722



3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735

3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
**     case=1           Issue a fossil_warning() while generating the page.
**     case=2           Extra db_begin_transaction()
**     case=3           Extra db_end_transaction()
**     case=4           Error during SQL processing
**     case=5           Call the segfault handler
**     case=6           Call webpage_assert()
**     case=7           Call webpage_error()



*/
void test_warning_page(void){
  int iCase = atoi(PD("case","0"));
  int i;
  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
  }
  style_set_current_feature("test");
  style_header("Warning Test Page");
  style_submenu_element("Error Log","%R/errorlog");
  if( iCase<1 || iCase>4 ){

    @ <p>Generate a message to the <a href="%R/errorlog">error log</a>
    @ by clicking on one of the following cases:
  }else{
    @ <p>This is the test page for case=%d(iCase).  All possible cases:
  }
  for(i=1; i<=8; i++){
    @ <a href='./test-warning?case=%d(i)'>[%d(i)]</a>
  }
  @ </p>
  @ <p><ol>
  @ <li value='1'> Call fossil_warning()
  if( iCase==1 ){
    fossil_warning("Test warning message from /test-warning");







>
>
>












|
>
|
|
<
<
<
|







3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741



3742
3743
3744
3745
3746
3747
3748
3749
**     case=1           Issue a fossil_warning() while generating the page.
**     case=2           Extra db_begin_transaction()
**     case=3           Extra db_end_transaction()
**     case=4           Error during SQL processing
**     case=5           Call the segfault handler
**     case=6           Call webpage_assert()
**     case=7           Call webpage_error()
**     case=8           Simulate a timeout
**     case=9           Simulate a TH1 XSS vulnerability
**     case=10          Simulate a TH1 SQL-injection vulnerability
*/
void test_warning_page(void){
  int iCase = atoi(PD("case","0"));
  int i;
  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
  }
  style_set_current_feature("test");
  style_header("Warning Test Page");
  style_submenu_element("Error Log","%R/errorlog");
  @ <p>This page will generate various kinds of errors to test Fossil's
  @ reaction.  Depending on settings, a message might be written
  @ into the <a href="%R/errorlog">error log</a>.  Click on
  @ one of the following hyperlinks to generate a simulated error:



  for(i=1; i<=10; i++){
    @ <a href='./test-warning?case=%d(i)'>[%d(i)]</a>
  }
  @ </p>
  @ <p><ol>
  @ <li value='1'> Call fossil_warning()
  if( iCase==1 ){
    fossil_warning("Test warning message from /test-warning");
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783



















3784
3785
3786
3787
3788
  if( iCase==5 ){
    sigsegv_handler(0);
  }
  @ <li value='6'> call webpage_assert(0)
  if( iCase==6 ){
    webpage_assert( 5==7 );
  }
  @ <li value='7'> call webpage_error()"
  if( iCase==7 ){
    cgi_reset_content();
    webpage_error("Case 7 from /test-warning");
  }
  @ <li value='8'> simulated timeout"
  if( iCase==8 ){
    fossil_set_timeout(1);
    cgi_reset_content();
    sqlite3_sleep(1100);



















  }
  @ </ol>
  @ <p>End of test</p>
  style_finish_page();
}







|




|




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





3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
  if( iCase==5 ){
    sigsegv_handler(0);
  }
  @ <li value='6'> call webpage_assert(0)
  if( iCase==6 ){
    webpage_assert( 5==7 );
  }
  @ <li value='7'> call webpage_error()
  if( iCase==7 ){
    cgi_reset_content();
    webpage_error("Case 7 from /test-warning");
  }
  @ <li value='8'> simulated timeout
  if( iCase==8 ){
    fossil_set_timeout(1);
    cgi_reset_content();
    sqlite3_sleep(1100);
  }
  @ <li value='9'> simulated TH1 XSS vulnerability
  @ <li value='10'> simulated TH1 SQL-injection vulnerability
  if( iCase==9 || iCase==10 ){
    const char *zR;
    int n, rc;
    static const char *zTH1[] = {
       /* case 9 */  "html [taint {<b>XSS</b>}]",
       /* case 10 */ "query [taint {SELECT 'SQL-injection' AS msg}] {\n"
                     "  html \"<b>[htmlize $msg]</b>\"\n"
                     "}"
    };
    rc = Th_Eval(g.interp, 0, zTH1[iCase==10], -1);
    zR = Th_GetResult(g.interp, &n);
    if( rc==TH_OK ){
      @ <pre class="th1result">%h(zR)</pre>
    }else{
      @ <pre class="th1error">%h(zR)</pre>
    }
  }
  @ </ol>
  @ <p>End of test</p>
  style_finish_page();
}
Changes to src/security_audit.c.
808
809
810
811
812
813
814
815
816
817
818
819
820

821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841

842
843
844
845
846
847
848

/*
** WEBPAGE: errorlog
**
** Show the content of the error log.  Only the administrator can view
** this page.
**
**    y=0x01          Show only hack attempts
**    y=0x02          Show only panics and assertion faults
**    y=0x04          Show hung backoffice processes
**    y=0x08          Show POST requests from a different origin
**    y=0x10          Show SQLITE_AUTH and similar
**    y=0x20          Show SMTP error reports

**    y=0x40          Show other uncategorized messages
**
** If y is omitted or is zero, a count of the various message types is
** shown.
*/
void errorlog_page(void){
  i64 szFile;
  FILE *in;
  char *zLog;
  const char *zType = P("y");
  static const int eAllTypes = 0x7f;
  long eType = 0;
  int bOutput = 0;
  int prevWasTime = 0;
  int nHack = 0;
  int nPanic = 0;
  int nOther = 0;
  int nHang = 0;
  int nXPost = 0;
  int nAuth = 0;
  int nSmtp = 0;

  char z[10000];
  char zTime[10000];

  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;







|
|
|
|
|
|
>
|









|










>







808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850

/*
** WEBPAGE: errorlog
**
** Show the content of the error log.  Only the administrator can view
** this page.
**
**    y=0x001          Show only hack attempts
**    y=0x002          Show only panics and assertion faults
**    y=0x004          Show hung backoffice processes
**    y=0x008          Show POST requests from a different origin
**    y=0x010          Show SQLITE_AUTH and similar
**    y=0x020          Show SMTP error reports
**    y=0x040          Show TH1 vulnerability reports
**    y=0x800          Show other uncategorized messages
**
** If y is omitted or is zero, a count of the various message types is
** shown.
*/
void errorlog_page(void){
  i64 szFile;
  FILE *in;
  char *zLog;
  const char *zType = P("y");
  static const int eAllTypes = 0x87f;
  long eType = 0;
  int bOutput = 0;
  int prevWasTime = 0;
  int nHack = 0;
  int nPanic = 0;
  int nOther = 0;
  int nHang = 0;
  int nXPost = 0;
  int nAuth = 0;
  int nSmtp = 0;
  int nVuln = 0;
  char z[10000];
  char zTime[10000];

  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
915
916
917
918
919
920
921



922
923
924
925
926
927
928
    if( eType & 0x10 ){
      @ <li>SQLITE_AUTH and similar errors
    }
    if( eType & 0x20 ){
      @ <li>SMTP malfunctions
    }
    if( eType & 0x40 ){



      @ <li>Other uncategorized messages
    }
    @ </ul>
  }
  @ <hr>
  if( eType ){
    @ <pre>







>
>
>







917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
    if( eType & 0x10 ){
      @ <li>SQLITE_AUTH and similar errors
    }
    if( eType & 0x20 ){
      @ <li>SMTP malfunctions
    }
    if( eType & 0x40 ){
      @ <li>TH1 vulnerabilities
    }
    if( eType & 0x800 ){
      @ <li>Other uncategorized messages
    }
    @ </ul>
  }
  @ <hr>
  if( eType ){
    @ <pre>
951
952
953
954
955
956
957




958
959
960
961
962
963
964
965
966
      }else
      if( sqlite3_strglob("SECURITY: authorizer blocks*",z)==0
       || sqlite3_strglob("warning: SQLITE_AUTH*",z)==0
      ){
        bOutput = (eType & 0x10)!=0;
        nAuth++;
      }else




      {
        bOutput = (eType & 0x40)!=0;
        nOther++;
      }
      if( bOutput ){
        @ %h(zTime)\
      }
    }
    if( strncmp(z, "--------", 8)==0 ){







>
>
>
>

|







956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
      }else
      if( sqlite3_strglob("SECURITY: authorizer blocks*",z)==0
       || sqlite3_strglob("warning: SQLITE_AUTH*",z)==0
      ){
        bOutput = (eType & 0x10)!=0;
        nAuth++;
      }else
      if( strncmp(z,"possible", 8)==0 && strstr(z,"tainted")!=0 ){
        bOutput = (eType & 0x40)!=0;
        nVuln++;
      }else
      {
        bOutput = (eType & 0x800)!=0;
        nOther++;
      }
      if( bOutput ){
        @ %h(zTime)\
      }
    }
    if( strncmp(z, "--------", 8)==0 ){
976
977
978
979
980
981
982
983
984
985
986
987
988
989




990
991
992
993
994
995
996
    }
  }
  fclose(in);
  if( eType ){
    @ </pre>
  }
  if( eType==0 ){
    int nNonHack = nPanic + nHang + nAuth + nSmtp + nOther;
    int nTotal = nNonHack + nHack + nXPost;
    @ <p><table border="a" cellspacing="0" cellpadding="5">
    if( nPanic>0 ){
      @ <tr><td align="right">%d(nPanic)</td>
      @     <td><a href="./errorlog?y=2">Panics</a></td>
    }




    if( nHack>0 ){
      @ <tr><td align="right">%d(nHack)</td>
      @     <td><a href="./errorlog?y=1">Hack Attempts</a></td>
    }
    if( nHang>0 ){
      @ <tr><td align="right">%d(nHang)</td>
      @     <td><a href="./errorlog?y=4">Hung Backoffice</a></td>







|






>
>
>
>







985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
    }
  }
  fclose(in);
  if( eType ){
    @ </pre>
  }
  if( eType==0 ){
    int nNonHack = nPanic + nHang + nAuth + nSmtp + nVuln + nOther;
    int nTotal = nNonHack + nHack + nXPost;
    @ <p><table border="a" cellspacing="0" cellpadding="5">
    if( nPanic>0 ){
      @ <tr><td align="right">%d(nPanic)</td>
      @     <td><a href="./errorlog?y=2">Panics</a></td>
    }
    if( nVuln>0 ){
      @ <tr><td align="right">%d(nVuln)</td>
      @     <td><a href="./errorlog?y=64">TH1 Vulnerabilities</a></td>
    }
    if( nHack>0 ){
      @ <tr><td align="right">%d(nHack)</td>
      @     <td><a href="./errorlog?y=1">Hack Attempts</a></td>
    }
    if( nHang>0 ){
      @ <tr><td align="right">%d(nHang)</td>
      @     <td><a href="./errorlog?y=4">Hung Backoffice</a></td>
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
    }
    if( nSmtp>0 ){
      @ <tr><td align="right">%d(nSmtp)</td>
      @     <td><a href="./errorlog?y=32">SMTP faults</a></td>
    }
    if( nOther>0 ){
      @ <tr><td align="right">%d(nOther)</td>
      @     <td><a href="./errorlog?y=64">Other</a></td>
    }
    @ <tr><td align="right">%d(nTotal)</td>
    if( nTotal>0 ){
      @     <td><a href="./errorlog?y=255">All Messages</a></td>
    }else{
      @     <td>All Messages</td>
    }
    @ </table>
  }
  style_finish_page();
}







|



|







1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
    }
    if( nSmtp>0 ){
      @ <tr><td align="right">%d(nSmtp)</td>
      @     <td><a href="./errorlog?y=32">SMTP faults</a></td>
    }
    if( nOther>0 ){
      @ <tr><td align="right">%d(nOther)</td>
      @     <td><a href="./errorlog?y=2048">Other</a></td>
    }
    @ <tr><td align="right">%d(nTotal)</td>
    if( nTotal>0 ){
      @     <td><a href="./errorlog?y=4095">All Messages</a></td>
    }else{
      @     <td>All Messages</td>
    }
    @ </table>
  }
  style_finish_page();
}
Changes to src/th.c.
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944

 finish:
  thBufferFree(interp, &strbuf);
  thBufferFree(interp, &lenbuf);
  return rc;
}

/*
** Report misuse of a tainted string.
**
** In the current implementation, this routine issues a warning to the
** error log and returns 0, causing processing to continue.  This is so
** that the new taint detection will not disrupt legacy configurations.
** However, if modified so that this routine returns non-zero, then it
** will cause an error in the script.
*/
int Th_ReportTaint(
  Th_Interp *interp,       /* Report error here, if an error is reported */
  const char *zWhere,      /* Where the tainted string appears */
  const char *zStr,        /* The tainted string */
  int nStr                 /* Length of the tainted string */
){
  nStr = TH1_LEN(nStr);
  if( nStr>0 ){
    fossil_errorlog("warning: tainted %s: \"%.*s\"", zWhere, nStr, zStr);
  }else{
    fossil_errorlog("warning: tainted %s", zWhere);
  }
  return 0;
}

/*
** Evaluate the th1 script contained in the string (zProgram, nProgram)
** in the current stack frame.
*/
static int thEvalLocal(Th_Interp *interp, const char *zProgram, int nProgram){
  int rc = TH_OK;
  const char *zInput = zProgram;







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







907
908
909
910
911
912
913
























914
915
916
917
918
919
920

 finish:
  thBufferFree(interp, &strbuf);
  thBufferFree(interp, &lenbuf);
  return rc;
}

























/*
** Evaluate the th1 script contained in the string (zProgram, nProgram)
** in the current stack frame.
*/
static int thEvalLocal(Th_Interp *interp, const char *zProgram, int nProgram){
  int rc = TH_OK;
  const char *zInput = zProgram;
Changes to src/th_main.c.
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402


403
404
405
406
407
408
409
static void sendText(Blob *pOut, const char *z, int n, int encode){
  if(0==pOut && pThOut!=0){
    pOut = pThOut;
  }
  if(TH_INIT_NO_ENCODE & g.th1Flags){
    encode = 0;
  }
  if( encode==0 && n>0 && TH1_TAINTED(n) ){
    if( Th_ReportTaint(0, "output string", z, n) ){
      return;
    }
    n = TH1_LEN(n);
  }
  if( enableOutput && n ){
    if( n<0 ){
      n = strlen(z);


    }
    if( encode ){
      z = htmlize(z, n);
      n = strlen(z);
    }
    if(pOut!=0){
      blob_append(pOut, z, n);







<
<
<
<
<
<



>
>







387
388
389
390
391
392
393






394
395
396
397
398
399
400
401
402
403
404
405
static void sendText(Blob *pOut, const char *z, int n, int encode){
  if(0==pOut && pThOut!=0){
    pOut = pThOut;
  }
  if(TH_INIT_NO_ENCODE & g.th1Flags){
    encode = 0;
  }






  if( enableOutput && n ){
    if( n<0 ){
      n = strlen(z);
    }else{
      n = TH1_LEN(n);
    }
    if( encode ){
      z = htmlize(z, n);
      n = strlen(z);
    }
    if(pOut!=0){
      blob_append(pOut, z, n);
532
533
534
535
536
537
538


539
540
541







542
543
544
545
546
547
548
549
static int putsCmd(
  Th_Interp *interp,
  void *pConvert,
  int argc,
  const char **argv,
  int *argl
){


  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "puts STRING");
  }







  sendText(0,(char*)argv[1], argl[1], *(unsigned int*)pConvert);
  return TH_OK;
}

/*
** TH1 command: redirect URL ?withMethod?
**
** Issues an HTTP redirect to the specified URL and then exits the process.







>
>



>
>
>
>
>
>
>
|







528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
static int putsCmd(
  Th_Interp *interp,
  void *pConvert,
  int argc,
  const char **argv,
  int *argl
){
  int encode = *(unsigned int*)pConvert;
  int n;
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "puts STRING");
  }
  n = argl[1];
  if( encode==0 && n>0 && TH1_TAINTED(n) ){
    if( Th_ReportTaint(interp, "output string", argv[1], n) ){
      return TH_ERROR;
    }
    n = TH1_LEN(n);
  }
  sendText(0,(char*)argv[1], n, encode);
  return TH_OK;
}

/*
** TH1 command: redirect URL ?withMethod?
**
** Issues an HTTP redirect to the specified URL and then exits the process.
3036
3037
3038
3039
3040
3041
3042



































































3043
3044
3045
3046
3047
3048
3049
    ** Th_SetOutputBlob() has been called. If it has not been called,
    ** pThOut will be 0, which will redirect the output to CGI/stdout,
    ** as appropriate. We need to pass on g.th1Flags for the case of
    ** recursive calls, so that, e.g., TH_INIT_NO_ENCODE does not get
    ** inadvertently toggled off by a recursive call.
    */;
}




































































/*
** COMMAND: test-th-render
**
** Usage: %fossil test-th-render FILE
**
** Read the content of the file named "FILE" as if it were a header or







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







3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
    ** Th_SetOutputBlob() has been called. If it has not been called,
    ** pThOut will be 0, which will redirect the output to CGI/stdout,
    ** as appropriate. We need to pass on g.th1Flags for the case of
    ** recursive calls, so that, e.g., TH_INIT_NO_ENCODE does not get
    ** inadvertently toggled off by a recursive call.
    */;
}

/*
** SETTING: vuln-report           width=8 default=log
**
** This setting controls Fossil's behavior when it encounters a potential
** XSS or SQL-injection vulnerability due to misuse of TH1 configuration
** scripts.  Choices are:
**
**    off            Do nothing.  Ignore the vulnerability.
**
**    log            Write a report of the problem into the error log.
**
**    block          Like "log" but also prevent the offending TH1 command
**                   from running.
**
**    fatal          Render an error message page instead of the requested
**                   page.
*/

/*
** Report misuse of a tainted string in TH1.
**
** The behavior depends on the vuln-report setting.  If "off", this routine
** is a no-op.  Otherwise, right a message into the error log.  If
** vuln-report is "log", that is all that happens.  But for any other
** value of vuln-report, a fatal error is raised.
*/
int Th_ReportTaint(
  Th_Interp *interp,       /* Report error here, if an error is reported */
  const char *zWhere,      /* Where the tainted string appears */
  const char *zStr,        /* The tainted string */
  int nStr                 /* Length of the tainted string */
){
  char *zDisp;             /* Dispensation */
  const char *zVulnType;   /* Type of vulnerability */

  zDisp = db_get("vuln-report","log");
  if( is_false(zDisp) ) return 0;
  if( strstr(zWhere,"SQL")!=0 ){
    zVulnType = "SQL-injection";
  }else{
    zVulnType = "XSS";
  }
  nStr = TH1_LEN(nStr);
  fossil_errorlog("possible %s vulnerability due to tainted TH1 %s: \"%.*s\"",
                  zVulnType, zWhere, nStr, zStr);
  if( strcmp(zDisp,"log")==0 ){
    return 0;
  }
  if( strcmp(zDisp,"block")==0 ){
    char *z = mprintf("tainted %s: \"", zWhere);
    Th_ErrorMessage(interp, z, zStr, nStr);
    fossil_free(z);
  }else{
    char *z = mprintf("%#h", nStr, zStr);
    cgi_reset_content();
    style_submenu_enable(0);
    style_set_current_feature("error");
    style_header("Configuration Error");
    @ <p>Error in a TH1 configuration script: 
    @ tainted %h(zWhere): "%z(z)"
    style_finish_page();
    cgi_reply();
    fossil_exit(1);
  }
  return 1;
}

/*
** COMMAND: test-th-render
**
** Usage: %fossil test-th-render FILE
**
** Read the content of the file named "FILE" as if it were a header or