%#h} \n", argl[2], argv[2]);
+ Th_Trace("query_eval {%#h } \n",TH1_LEN(argl[2]),argv[2]);
}
- res = Th_Eval(interp, 0, argv[2], argl[2]);
+ res = Th_Eval(interp, 0, argv[2], TH1_LEN(argl[2]));
if( g.thTrace ){
int nTrRes;
char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes);
Th_Trace("[query_eval] => %h {%#h} \n",
- Th_ReturnCodeName(res, 0), nTrRes, zTrRes);
+ Th_ReturnCodeName(res, 0), TH1_LEN(nTrRes), zTrRes);
}
if( res==TH_BREAK || res==TH_CONTINUE ) res = TH_OK;
}
rc = sqlite3_finalize(pStmt);
if( rc!=SQLITE_OK ){
@@ -2038,11 +2108,11 @@
Th_SetResult(interp, 0, 0);
rc = TH_OK;
}
if( g.thTrace ){
Th_Trace("[setting %s%#h] => %d \n", strict ? "strict " : "",
- argl[nArg], argv[nArg], rc);
+ TH1_LEN(argl[nArg]), argv[nArg], rc);
}
return rc;
}
/*
@@ -2121,11 +2191,11 @@
return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS);
}
zErr = re_compile(&pRe, argv[nArg], noCase);
if( !zErr ){
Th_SetResultInt(interp, re_match(pRe,
- (const unsigned char *)argv[nArg+1], argl[nArg+1]));
+ (const unsigned char *)argv[nArg+1], TH1_LEN(argl[nArg+1])));
rc = TH_OK;
}else{
Th_SetResult(interp, zErr, -1);
rc = TH_ERROR;
}
@@ -2160,11 +2230,11 @@
UrlData urlData;
if( argc<2 || argc>5 ){
return Th_WrongNumArgs(interp, HTTP_WRONGNUMARGS);
}
- if( fossil_strnicmp(argv[nArg], "-asynchronous", argl[nArg])==0 ){
+ if( fossil_strnicmp(argv[nArg], "-asynchronous", TH1_LEN(argl[nArg]))==0 ){
fAsynchronous = 1; nArg++;
}
if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++;
if( nArg+1!=argc && nArg+2!=argc ){
return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS);
@@ -2189,11 +2259,11 @@
return TH_ERROR;
}
re_free(pRe);
blob_zero(&payload);
if( nArg+2==argc ){
- blob_append(&payload, argv[nArg+1], argl[nArg+1]);
+ blob_append(&payload, argv[nArg+1], TH1_LEN(argl[nArg+1]));
zType = "POST";
}else{
zType = "GET";
}
if( fAsynchronous ){
@@ -2268,11 +2338,11 @@
if( argc!=2 ){
return Th_WrongNumArgs(interp, "captureTh1 STRING");
}
pOrig = Th_SetOutputBlob(&out);
zStr = argv[1];
- nStr = argl[1];
+ nStr = TH1_LEN(argl[1]);
rc = Th_Eval(g.interp, 0, zStr, nStr);
Th_SetOutputBlob(pOrig);
if(0==rc){
Th_SetResult(g.interp, blob_str(&out), blob_size(&out));
}
@@ -2387,13 +2457,15 @@
{"setting", settingCmd, 0},
{"styleFooter", styleFooterCmd, 0},
{"styleHeader", styleHeaderCmd, 0},
{"styleScript", styleScriptCmd, 0},
{"submenu", submenuCmd, 0},
+ {"taint", taintCmd, 0},
{"tclReady", tclReadyCmd, 0},
{"trace", traceCmd, 0},
{"stime", stimeCmd, 0},
+ {"untaint", untaintCmd, 0},
{"unversioned", unversionedCmd, 0},
{"utime", utimeCmd, 0},
{"verifyCsrf", verifyCsrfCmd, 0},
{"verifyLogin", verifyLoginCmd, 0},
{"wiki", wikiCmd, (void*)&aFlags[0]},
@@ -2494,10 +2566,26 @@
Th_Trace("set %h {%h} \n", zName, zValue);
}
Th_SetVar(g.interp, zName, -1, zValue, strlen(zValue));
}
}
+
+/*
+** Store a string value in a variable in the interpreter
+** with the "taint" marking, so that TH1 knows that this
+** variable contains content under the control of the remote
+** user and presents a risk of XSS or SQL-injection attacks.
+*/
+void Th_StoreUnsafe(const char *zName, const char *zValue){
+ Th_FossilInit(TH_INIT_DEFAULT);
+ if( zValue ){
+ if( g.thTrace ){
+ Th_Trace("set %h [taint {%h}] \n", zName, zValue);
+ }
+ Th_SetVar(g.interp, zName, -1, zValue, TH1_ADD_TAINT(strlen(zValue)));
+ }
+}
/*
** Appends an element to a TH1 list value. This function is called by the
** transfer subsystem; therefore, it must be very careful to avoid doing
** any unnecessary work. To that end, the TH1 subsystem will not be called
@@ -2680,10 +2768,11 @@
char *zResult = (char*)Th_GetResult(g.interp, &nResult);
/*
** Make sure that the TH1 script error was not caused by a "missing"
** command hook handler as that is not actually an error condition.
*/
+ nResult = TH1_LEN(nResult);
if( memcmp(zResult, NO_COMMAND_HOOK_ERROR, nResult)!=0 ){
sendError(0,zResult, nResult, 0);
}else{
/*
** There is no command hook handler "installed". This situation
@@ -2767,10 +2856,11 @@
char *zResult = (char*)Th_GetResult(g.interp, &nResult);
/*
** Make sure that the TH1 script error was not caused by a "missing"
** webpage hook handler as that is not actually an error condition.
*/
+ nResult = TH1_LEN(nResult);
if( memcmp(zResult, NO_WEBPAGE_HOOK_ERROR, nResult)!=0 ){
sendError(0,zResult, nResult, 1);
}else{
/*
** There is no webpage hook handler "installed". This situation
@@ -2907,11 +2997,11 @@
rc = Th_Eval(g.interp, 0, (const char*)z, i);
if( g.thTrace ){
int nTrRes;
char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes);
Th_Trace("[render_eval] => %h {%#h} \n",
- Th_ReturnCodeName(rc, 0), nTrRes, zTrRes);
+ Th_ReturnCodeName(rc, 0), TH1_LEN(nTrRes), zTrRes);
}
if( rc!=TH_OK ) break;
z += i;
if( z[0] ){ z += 6; }
i = 0;
@@ -2953,10 +3043,77 @@
** 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");
+ @ 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
@@ -2992,10 +3149,11 @@
if( find_option("set-user-caps", 0, 0)!=0 ){
const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS");
login_set_capabilities(zCap ? zCap : "sx", 0);
g.useLocalauth = 1;
}
+ db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0);
verify_all_options();
if( g.argc<3 ){
usage("FILE");
}
blob_zero(&in);
@@ -3044,10 +3202,11 @@
if( find_option("set-user-caps", 0, 0)!=0 ){
const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS");
login_set_capabilities(zCap ? zCap : "sx", 0);
g.useLocalauth = 1;
}
+ db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0);
verify_all_options();
if( g.argc!=3 ){
usage("script");
}
if(file_isfile(g.argv[2], ExtFILE)){
Index: src/th_tcl.c
==================================================================
--- src/th_tcl.c
+++ src/th_tcl.c
@@ -41,16 +41,16 @@
#define USE_ARGV_TO_OBJV() \
int objc; \
Tcl_Obj **objv; \
int obji;
-#define COPY_ARGV_TO_OBJV() \
- objc = argc-1; \
- objv = (Tcl_Obj **)ckalloc((unsigned)(objc * sizeof(Tcl_Obj *))); \
- for(obji=1; objixPostEval : tclContext->xPreEval;
if( xNotifyProc ){
rc = xNotifyProc(bIsPost ?
tclContext->pPostContext : tclContext->pPreContext,
- interp, ctx, argc, argv, argl, rc);
+ interp, ctx, argc, argv, TH1_LEN(argl), rc);
}
return rc;
}
/*
@@ -485,17 +485,17 @@
tclInterp = GET_CTX_TCL_INTERP(ctx);
if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
return TH_ERROR;
}
- rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc);
+ rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, TH1_LEN(argl), rc);
if( rc!=TH_OK ){
return rc;
}
Tcl_Preserve((ClientData)tclInterp);
if( argc==2 ){
- objPtr = Tcl_NewStringObj(argv[1], argl[1]);
+ objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1]));
Tcl_IncrRefCount(objPtr);
rc = Tcl_EvalObjEx(tclInterp, objPtr, 0);
Tcl_DecrRefCount(objPtr); objPtr = 0;
}else{
USE_ARGV_TO_OBJV();
@@ -507,11 +507,11 @@
FREE_ARGV_TO_OBJV();
}
zResult = getTclResult(tclInterp, &nResult);
Th_SetResult(interp, zResult, nResult);
Tcl_Release((ClientData)tclInterp);
- rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl,
+ rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, TH1_LEN(argl),
getTh1ReturnCode(rc));
return rc;
}
/*
@@ -545,17 +545,17 @@
tclInterp = GET_CTX_TCL_INTERP(ctx);
if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
return TH_ERROR;
}
- rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc);
+ rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, TH1_LEN(argl), rc);
if( rc!=TH_OK ){
return rc;
}
Tcl_Preserve((ClientData)tclInterp);
if( argc==2 ){
- objPtr = Tcl_NewStringObj(argv[1], argl[1]);
+ objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1]));
Tcl_IncrRefCount(objPtr);
rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr);
Tcl_DecrRefCount(objPtr); objPtr = 0;
}else{
USE_ARGV_TO_OBJV();
@@ -574,11 +574,11 @@
Th_SetResult(interp, zResult, nResult);
if( rc==TCL_OK ){
Tcl_DecrRefCount(resultObjPtr); resultObjPtr = 0;
}
Tcl_Release((ClientData)tclInterp);
- rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl,
+ rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, TH1_LEN(argl),
getTh1ReturnCode(rc));
return rc;
}
/*
@@ -610,20 +610,20 @@
tclInterp = GET_CTX_TCL_INTERP(ctx);
if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
return TH_ERROR;
}
- rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc);
+ rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, TH1_LEN(argl), rc);
if( rc!=TH_OK ){
return rc;
}
Tcl_Preserve((ClientData)tclInterp);
#if !defined(USE_TCL_EVALOBJV) || !USE_TCL_EVALOBJV
if( GET_CTX_TCL_USEOBJPROC(ctx) ){
Tcl_Command command;
Tcl_CmdInfo cmdInfo;
- Tcl_Obj *objPtr = Tcl_NewStringObj(argv[1], argl[1]);
+ Tcl_Obj *objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1]));
Tcl_IncrRefCount(objPtr);
command = Tcl_GetCommandFromObj(tclInterp, objPtr);
if( !command || Tcl_GetCommandInfoFromToken(command, &cmdInfo)==0 ){
Th_ErrorMessage(interp, "Tcl command not found:", argv[1], argl[1]);
Tcl_DecrRefCount(objPtr); objPtr = 0;
@@ -649,11 +649,11 @@
FREE_ARGV_TO_OBJV();
}
zResult = getTclResult(tclInterp, &nResult);
Th_SetResult(interp, zResult, nResult);
Tcl_Release((ClientData)tclInterp);
- rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl,
+ rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, TH1_LEN(argl),
getTh1ReturnCode(rc));
return rc;
}
/*
@@ -782,11 +782,11 @@
return TCL_ERROR;
}
arg = Tcl_GetStringFromObj(objv[1], &nArg);
rc = Th_Eval(th1Interp, 0, arg, nArg);
arg = Th_GetResult(th1Interp, &nArg);
- Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, nArg));
+ Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, TH1_LEN(nArg)));
return getTclReturnCode(rc);
}
/*
** Tcl command: th1Expr arg
@@ -815,11 +815,11 @@
return TCL_ERROR;
}
arg = Tcl_GetStringFromObj(objv[1], &nArg);
rc = Th_Expr(th1Interp, arg, nArg);
arg = Th_GetResult(th1Interp, &nArg);
- Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, nArg));
+ Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, TH1_LEN(nArg)));
return getTclReturnCode(rc);
}
/*
** Array of Tcl integration commands. Used when adding or removing the Tcl
Index: src/timeline.c
==================================================================
--- src/timeline.c
+++ src/timeline.c
@@ -1888,11 +1888,11 @@
if( zTagName ){
zType = "ci";
if( matchStyle==MS_EXACT ){
/* For exact maching, inhibit links to the selected tag. */
zThisTag = zTagName;
- Th_Store("current_checkin", zTagName);
+ Th_StoreUnsafe("current_checkin", zTagName);
}
/* Display a checkbox to enable/disable display of related check-ins. */
if( advancedMenu ){
style_submenu_checkbox("rel", "Related", 0, 0);
Index: src/tkt.c
==================================================================
--- src/tkt.c
+++ src/tkt.c
@@ -210,21 +210,21 @@
zVal = zRevealed = db_reveal(zVal);
}
if( (j = fieldId(zName))>=0 ){
aField[j].zValue = mprintf("%s", zVal);
}else if( memcmp(zName, "tkt_", 4)==0 && Th_Fetch(zName, &size)==0 ){
- Th_Store(zName, zVal);
+ Th_StoreUnsafe(zName, zVal);
}
free(zRevealed);
}
Th_Store("tkt_mage", human_readable_age(db_column_double(&q, 2)));
Th_Store("tkt_cage", human_readable_age(db_column_double(&q, 3)));
}
db_finalize(&q);
for(i=0; i\n",
- argl[1], argv[1], argl[2], argv[2]);
+ TH1_LEN(argl[1]), argv[1], TH1_LEN(argl[2]), argv[2]);
}
for(idx=0; idx=nField ){
Th_ErrorMessage(g.interp, "no such TICKET column: ", argv[1], argl[1]);
@@ -938,10 +938,11 @@
const char *zValue;
int nValue;
if( aField[i].zAppend ) continue;
zValue = Th_Fetch(aField[i].zName, &nValue);
if( zValue ){
+ nValue = TH1_LEN(nValue);
while( nValue>0 && fossil_isspace(zValue[nValue-1]) ){ nValue--; }
if( ((aField[i].mUsed & USEDBY_TICKETCHNG)!=0 && nValue>0)
|| memcmp(zValue, aField[i].zValue, nValue)!=0
||(int)strlen(aField[i].zValue)!=nValue
){
@@ -1040,16 +1041,16 @@
if( uid ){
char * zEmail =
db_text(0, "SELECT find_emailaddr(info) FROM user WHERE uid=%d",
uid);
if( zEmail ){
- Th_Store("private_contact", zEmail);
+ Th_StoreUnsafe("private_contact", zEmail);
fossil_free(zEmail);
}
}
}
- Th_Store("login", login_name());
+ Th_StoreUnsafe("login", login_name());
Th_Store("date", db_text(0, "SELECT datetime('now')"));
Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd,
(void*)&zNewUuid, 0);
if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT \n", -1);
if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){
@@ -1120,11 +1121,11 @@
initializeVariablesFromDb();
if( g.zPath[0]=='d' ) showAllFields();
form_begin(0, "%R/%s", g.zPath);
@
zScript = ticket_editpage_code();
- Th_Store("login", login_name());
+ Th_StoreUnsafe("login", login_name());
Th_Store("date", db_text(0, "SELECT datetime('now')"));
Th_CreateCommand(g.interp, "append_field", appendRemarkCmd, 0, 0);
Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, (void*)&zName,0);
if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT_SCRIPT \n", -1);
if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zName ){
Index: src/tktsetup.c
==================================================================
--- src/tktsetup.c
+++ src/tktsetup.c
@@ -481,11 +481,11 @@
@
@ if {[info exists tkt_uuid]} {
@ html ""
@ copybtn hash-tk 0 $tkt_uuid 2
@ if {[hascap s]} {
-@ html " ($tkt_id)"
+@ puts " ($tkt_id)"
@ }
@ html " \n"
@ } else {
@ if {[hascap s]} {
@ html "Deleted "
@@ -522,24 +522,24 @@
@ $
@
@ Last Modified:
@
@ if {[info exists tkt_datetime]} {
-@ html $tkt_datetime
+@ puts $tkt_datetime
@ }
@ if {[info exists tkt_mage]} {
-@ html " $tkt_mage"
+@ html " [htmlize $tkt_mage] ago"
@ }
@
@
@ Created:
@
@ if {[info exists tkt_datetime_creation]} {
-@ html $tkt_datetime_creation
+@ puts $tkt_datetime_creation
@ }
@ if {[info exists tkt_cage]} {
-@ html " $tkt_cage"
+@ html " [htmlize $tkt_cage] ago"
@ }
@
@
@ enable_output [hascap e]
@
@@ -614,19 +614,19 @@
@ html "User Comments: \n"
@ html "\n"
@ set seenRow 1
@ }
@ html "\n"
@ if {$alwaysPlaintext || $xmimetype eq "text/plain"} {
@ set r [randhex]
-@ if {$xmimetype ne "text/plain"} {html "([htmlize $xmimetype])\n"}
+@ if {$xmimetype ne "text/plain"} {puts "($xmimetype)\n"}
@ wiki "[string trimright $xcomment] \n"
@ } elseif {$xmimetype eq "text/x-fossil-wiki"} {
@ wiki "\n[string trimright $xcomment]\n
\n"
@ } elseif {$xmimetype eq "text/x-markdown"} {
@ html [lindex [markdown $xcomment] 1]
@@ -801,19 +801,19 @@
@ html "Previous User Comments: \n"
@ html "\n"
@ set seenRow 1
@ }
@ html "\n"
@ if {$alwaysPlaintext || $xmimetype eq "text/plain"} {
@ set r [randhex]
-@ if {$xmimetype ne "text/plain"} {html "([htmlize $xmimetype])\n"}
+@ if {$xmimetype ne "text/plain"} {puts "($xmimetype)\n"}
@ wiki "[string trimright $xcomment] \n"
@ } elseif {$xmimetype eq "text/x-fossil-wiki"} {
@ wiki "\n[string trimright $xcomment]\n
\n"
@ } elseif {$xmimetype eq "text/x-markdown"} {
@ html [lindex [markdown $xcomment] 1]