a53b9958fa734f73aeffb9ddfe2fbad1ca65f90c galt Mon Jan 30 16:18:41 2017 -0800 Check-in of CSP2 Content-Security-Policy work. All C-language CGIs should now support CSP2 in browser to stop major forms of XSS javascript injection. Javascript on pages is gathered together, and then emitted in a single script block at the end with a nonce that tells the browser, this is js that we generated instead of being injected by a hacker. Both inline script from script blocks and inline js event handlers had to be pulled out and separated. You will not see js sprinkled through-out the page now. Older browsers that support CSP1 or that do not understand CSP at all will still work, just without protection. External js libraries loaded at runtime need to be added to the CSP policy header in src/lib/htmshell.c. diff --git src/lib/cheapcgi.c src/lib/cheapcgi.c index ad4a545..fbeea5b 100644 --- src/lib/cheapcgi.c +++ src/lib/cheapcgi.c @@ -5,30 +5,230 @@ * granted for all use - public, private or commercial. */ #include "common.h" #include "hash.h" #include "cheapcgi.h" #include "portable.h" #include "linefile.h" #include "errAbort.h" #include "filePath.h" #include "htmshell.h" #ifndef GBROWSE #include "mime.h" #endif /* GBROWSE */ #include +//============ javascript inline-separation routines =============== + +// One of the main services that CSP (Content Security Policy) provides +// is protecting from reflected and stored XSS attacks by disabling all inline javacript, +// both in script tags, and in inline event handlers. The separated javascript +// can be either added back to the end of the html page with a nonce or sha hashid, +// or it can be saved to a temp file in trash and then included as a non-inline, off-page .js. + +// TODO make other versions that capture the output to a temp file. + +/* OBSOLETE +// Auto-increment. This helps create unique ids for easily connecting inline js with the html element. + +int autoInc = 0; +int getAutoInc() +// Get auto-incrementing value. +{ +return autoInc++; +} + +char *getAutoIncId() +// Generate an automatically incrementing html id attribute value. +// For cases where the element is not given any id, generate a unique id automatically. +{ +char autoId[32]; +safef(autoId, sizeof autoId, "auto%d", getAutoInc()); +return cloneString(autoId); +} +END OBSOLETE */ + +struct dyString *jsInlineLines = NULL; + +void jsInlineInit() +/* init if needed */ +{ +if (!jsInlineLines) + { + jsInlineLines = dyStringNew(1024); + } +} + +void jsInline(char *javascript) +/* Add javascript text to output file or memory structure */ +{ +jsInlineInit(); // init if needed +dyStringAppend(jsInlineLines, javascript); +} + +boolean jsInlineFinishCalled = FALSE; + +void jsInlineFinish() +/* finish outputting accumulated inline javascript */ +{ +if (jsInlineFinishCalled) + { + // jsInlineFinish can be called multiple times when generating framesets or genomeSpace. + warn("jsInlineFinish() called already."); // TODO GALT + } +jsInlineInit(); // init if needed +printf("\n", getNonce(), jsInlineLines->string); +dyStringClear(jsInlineLines); +jsInlineFinishCalled = TRUE; +} + +void jsInlineReset() +/* used by genomeSpace to repeatedly output multiple pages to stdout */ +{ +jsInlineFinishCalled = FALSE; +} + +const char * const jsEvents[] = { +"abort", +"activate", +"afterprint", +"afterupdate", +"beforeactivate", +"beforecopy", +"beforecut", +"beforedeactivate", +"beforeeditfocus", +"beforepaste", +"beforeprint", +"beforeunload", +"beforeupdate", +"blur", +"bounce", +"cellchange", +"change", +"click", +"contextmenu", +"controlselect", +"copy", +"cut", +"dataavailable", +"datasetchanged", +"datasetcomplete", +"dblclick", +"deactivate", +"drag", +"dragend", +"dragenter", +"dragleave", +"dragover", +"dragstart", +"drop", +"error", +"errorupdate", +"filterchange", +"finish", +"focus", +"focusin", +"focusout", +"hashchange", +"help", +"input", +"keydown", +"keypress", +"keyup", +"load", +"losecapture", +"message", +"mousedown", +"mouseenter", +"mouseleave", +"mousemove", +"mouseout", +"mouseover", +"mouseup", +"mousewheel", +"move", +"moveend", +"movestart", +"offline", +"line", +"online", +"paste", +"propertychange", +"readystatechange", +"reset", +"resize", +"resizeend", +"resizestart", +"rowenter", +"rowexit", +"rowsdelete", +"rowsinserted", +"scroll", +"search", +"select", +"selectionchange", +"selectstart", +"start", +"stop", +"submit", +"unload", +"" }; + + +boolean findJsEvent(char *event) +/* see if it is in the list */ +{ +int i = 0; +while (TRUE) + { + const char *w = jsEvents[i]; + if (sameString(w, event)) + return TRUE; + if (sameString(w, "")) + return FALSE; + ++i; + } +return FALSE; // should never get here +} + +void checkValidEvent(char *event) +/* check if it is lowercase and a known valid event name */ +{ +char *temp = cloneString(event); +tolowers(temp); +if (!sameString(temp, event)) + warn("jsInline: javascript event %s should be given in lower-case", event); +event = temp; +if (!findJsEvent(event)) + warn("jsInline: unknown javascript event %s", event); +freeMem (event); +} + +void jsOnEventById(char *event, char *idText, char *jsText) +/* Add js mapping for inline event */ +{ +checkValidEvent(event); +struct dyString *javascript = dyStringNew(1024); // TODO XSS Filter the idText? +dyStringPrintf(javascript, "document.getElementById('%s').on%s = function() {%s};\n", idText, event, jsText); +jsInline(javascript->string); +dyStringFree(&javascript); +} + + +//============ END of javascript inline-separation routines =============== + /* These three variables hold the parsed version of cgi variables. */ static char *inputString = NULL; static unsigned long inputSize; static struct hash *inputHash = NULL; static struct cgiVar *inputList = NULL; static boolean haveCookiesHash = FALSE; static struct hash *cookieHash = NULL; static struct cgiVar *cookieList = NULL; /* should cheapcgi use temp files to store uploaded files */ static boolean doUseTempFile = FALSE; void dumpCookieList() @@ -1265,71 +1465,80 @@ void cgiMakeSubmitButton() /* Make 'submit' type button. */ { cgiMakeButton("Submit", "Submit"); } void cgiMakeResetButton() /* Make 'reset' type button. */ { printf(""); } void cgiMakeClearButton(char *form, char *field) /* Make button to clear a text field. */ { +char id[256]; +safef(id, sizeof id, "%s_clickBut", form); char javascript[1024]; - safef(javascript, sizeof(javascript), "document.%s.%s.value = ''; document.%s.submit();", form, field, form); -cgiMakeOnClickButton(javascript, " Clear "); +cgiMakeOnClickButton(id, javascript, " Clear "); } void cgiMakeButtonWithMsg(char *name, char *value, char *msg) /* Make 'submit' type button. Display msg on mouseover, if present*/ { -printf("", - name, value, - (msg ? " TITLE=\"" : ""), (msg ? msg : ""), (msg ? "\"" : "" )); +printf(""); } void cgiMakeButtonWithOnClick(char *name, char *value, char *msg, char *onClick) /* Make 'submit' type button, with onclick javascript */ { -printf("", - name, value, onClick, - (msg ? " TITLE=\"" : ""), (msg ? msg : ""), (msg ? "\"" : "" )); +printf(""); + +jsOnEventById("click", name, onClick); } void cgiMakeButton(char *name, char *value) /* Make 'submit' type button */ { cgiMakeButtonWithMsg(name, value, NULL); } -void cgiMakeOnClickButton(char *command, char *value) -/* Make 'push' type button with client side onClick (java)script. */ +void cgiMakeOnClickButton(char *id, char *command, char *value) +/* Make button with client side onClick javascript. */ { -printf("", value, command); +printf("", id, value); +jsOnEventById("click", id, command); } void cgiMakeOnClickSubmitButton(char *command, char *name, char *value) /* Make submit button with both variable name and value with client side * onClick (java)script. */ { -printf("", - name, value, command); +printf("", + name, name, value); +jsOnEventById("click", name, command); } void cgiMakeOptionalButton(char *name, char *value, boolean disabled) /* Make 'submit' type button that can be disabled. */ { printf(""); } void cgiMakeFileEntry(char *name) /* Make file entry box/browser */ { printf("", name); @@ -1391,43 +1600,57 @@ (msg ? " TITLE=\"" : ""), (msg ? msg : ""), (msg ? "\"" : "" ), text); } void cgiParagraph(char *text) /* Make text paragraph */ { printf("

%s\n", text); } void cgiMakeRadioButton(char *name, char *value, boolean checked) /* Make radio type button. A group of radio buttons should have the * same name but different values. The default selection should be * sent with checked on. */ { -printf("", name, value, - (checked ? "CHECKED" : "")); +printf(""); } -void cgiMakeOnClickRadioButton(char *name, char *value, boolean checked, - char *command) -/* Make radio type button with onClick command. +void cgiMakeOnEventRadioButtonWithClass(char *name, char *value, boolean checked, + char *class, char *event, char *command) +/* Make radio type button with an event and an optional class attribute. * A group of radio buttons should have the * same name but different values. The default selection should be - * sent with checked on. */ -{ -printf("", - name, value, command, (checked ? "CHECKED" : "")); + * sent with checked on. If class is non-null it is included. */ +{ +char temp[256]; +safef(temp, sizeof temp, "%s_%s", name, value); +char *valNoSpc = replaceChars(temp, " ", "_"); // replace spaces with underscore +char *id = replaceChars(valNoSpc, ".", "_"); // replace dots with underscore, js does not like dots in ids +freeMem(valNoSpc); +printf(""); +jsOnEventById(event, id, command); +freeMem(id); } char *cgiBooleanShadowPrefix() /* Prefix for shadow variable set with boolean variables. */ { return "boolshad."; } #define BOOLSHAD_EXTRA "class='cbShadow'" boolean cgiBooleanDefined(char *name) /* Return TRUE if boolean variable is defined (by * checking for shadow. */ { char buf[256]; safef(buf, sizeof(buf), "%s%s", cgiBooleanShadowPrefix(), name); @@ -1487,42 +1710,50 @@ cgiMakeCheckBox2Bool(name, checked, TRUE, id, NULL); } void cgiMakeCheckBox(char *name, boolean checked) /* Make check box. */ { cgiMakeCheckBox2Bool(name, checked, TRUE, NULL, NULL); } void cgiMakeCheckBoxEnabled(char *name, boolean checked, boolean enabled) /* Make check box, optionally enabled/disabled. */ { cgiMakeCheckBox2Bool(name, checked, enabled, NULL, NULL); } +// TODO hopefully make this OBSOLETE void cgiMakeCheckBoxJS(char *name, boolean checked, char *javascript) /* Make check box with javascript. */ { cgiMakeCheckBox2Bool(name,checked,TRUE,NULL,javascript); } +// TODO hopefully make this OBSOLETE void cgiMakeCheckBoxIdAndJS(char *name, boolean checked, char *id, char *javascript) /* Make check box with ID and javascript. */ { cgiMakeCheckBox2Bool(name,checked,TRUE,id,javascript); } +void cgiMakeCheckBoxIdAndMore(char *name, boolean checked, char *id, char *moreHtml) +/* Make check box with ID and extra (non-javascript) html. */ +{ +cgiMakeCheckBox2Bool(name,checked,TRUE,id,moreHtml); +} + void cgiMakeCheckBoxFourWay(char *name, boolean checked, boolean enabled, char *id, char *classes, char *moreHtml) /* Make check box - with fourWay functionality (checked/unchecked by enabled/disabled) * Also makes a shadow hidden variable that supports the 2 boolean states. */ { char shadName[256]; printf("\n"); } void cgiMakeTextVar(char *varName, char *initialVal, int charSize) /* Make a text control filled with initial value. If charSize * is zero it's calculated from initialVal size. */ { cgiMakeOnKeypressTextVar(varName, initialVal, charSize, NULL); } -void cgiMakeTextVarWithExtraHtml(char *varName, char *initialVal, int width, char *extra) +void cgiMakeTextVarWithExtraHtml(char *varName, char *initialVal, int width, char *event, char *javascript) /* Make a text control filled with initial value. */ { if (initialVal == NULL) initialVal = ""; if (width==0) width=strlen(initialVal)*10; if (width==0) width = 100; -htmlPrintf("\n"); +htmlPrintf("\n", + varName, varName, width, initialVal); +if (event) + jsOnEventById(event, varName, javascript); } void cgiMakeIntVarWithExtra(char *varName, int initialVal, int maxDigits, char *extra) /* Make a text control filled with initial value and optional extra HTML. */ { if (maxDigits == 0) maxDigits = 4; htmlPrintf("", // TODO XSS extra varName, maxDigits, initialVal, extra ? extra : ""); } void cgiMakeIntVar(char *varName, int initialVal, int maxDigits) /* Make a text control filled with initial value. */ { cgiMakeIntVarWithExtra(varName, initialVal, maxDigits, NULL); } @@ -1640,34 +1870,36 @@ if (max) width=strlen(max)*10; else { int sz=initialVal+1000; if (min) sz=atoi(min) + 1000; width = 10; while (sz/=10) width+=10; } } if (width < 65) width = 65; -printf("\n"); } void cgiMakeIntVarWithLimits(char *varName, int initialVal, char *title, int width, int min, int max) { char minLimit[20]; char maxLimit[20]; char *minStr=NULL; char *maxStr=NULL; if (min != NO_VALUE) { safef(minLimit,sizeof(minLimit),"%d",min); @@ -1716,34 +1948,36 @@ void cgiMakeDoubleVarInRange(char *varName, double initialVal, char *title, int width, char *min, char *max) /* Make a floating point control filled with initial value. If min and/or max are non-NULL will enforce range Requires utils.js jQuery.js and inputBox class */ { if (width==0) { if (max) width=strlen(max)*10; } if (width < 65) width = 65; -printf("\n"); } void cgiMakeDoubleVarWithLimits(char *varName, double initialVal, char *title, int width, double min, double max) { char minLimit[20]; char maxLimit[20]; char *minStr=NULL; char *maxStr=NULL; if ((int)min != NO_VALUE) { safef(minLimit,sizeof(minLimit),"%g",min); @@ -1770,62 +2004,69 @@ } void cgiMakeDoubleVarWithMax(char *varName, double initialVal, char *title, int width, double max) { char maxLimit[20]; char *maxStr=NULL; if ((int)max != NO_VALUE) { safef(maxLimit,sizeof(maxLimit),"%g",max); maxStr = maxLimit; } cgiMakeDoubleVarInRange(varName,initialVal,title,width,NULL,maxStr); } void cgiMakeDropListClassWithStyleAndJavascript(char *name, char *menu[], - int menuSize, char *checked, char *class, char *style,char *javascript) + int menuSize, char *checked, char *class, char *style, struct slPair *events) /* Make a drop-down list with names, text class, style and javascript. */ { int i; char *selString; if (checked == NULL) checked = menu[0]; printf("next) + { + jsOnEventById(e->name, name, e->val); + } + } if (style) printf(" style='%s'", style); -if (javascript) - printf(" %s", javascript); printf(">\n"); for (i=0; i%s\n", selString, menu[i]); } printf("\n"); } void cgiMakeDropListClassWithStyle(char *name, char *menu[], int menuSize, char *checked, char *class, char *style) /* Make a drop-down list with names, text class and style. */ { -cgiMakeDropListClassWithStyleAndJavascript(name,menu,menuSize,checked,class,style,""); +cgiMakeDropListClassWithStyleAndJavascript(name,menu,menuSize,checked,class,style,NULL); } void cgiMakeDropListClass(char *name, char *menu[], int menuSize, char *checked, char *class) /* Make a drop-down list with names. */ { cgiMakeDropListClassWithStyle(name, menu, menuSize, checked, class, NULL); } void cgiMakeDropList(char *name, char *menu[], int menuSize, char *checked) /* Make a drop-down list with names. * uses style "normalText" */ { cgiMakeDropListClass(name, menu, menuSize, checked, "normalText"); } @@ -1882,83 +2123,116 @@ printf(""); puts(""); char buf[512]; safef(buf, sizeof(buf), "%s%s", cgiMultListShadowPrefix(), name); cgiMakeHiddenVar(buf, "0"); } void cgiMakeCheckboxGroup(char *name, char *menu[], int menuSize, struct slName *checked, int tableColumns) /* Make a table of checkboxes that have the same variable name but different * values (same behavior as a multi-select input). */ { cgiMakeCheckboxGroupWithVals(name, menu, NULL, menuSize, checked, tableColumns); } -void cgiMakeDropListFull(char *name, char *menu[], char *values[], - int menuSize, char *checked, char *extraAttribs) -/* Make a drop-down list with names and values. */ +void cgiMakeDropListFullExt(char *name, char *menu[], char *values[], + int menuSize, char *checked, char *event, char *javascript, char *style, char *class) +/* Make a drop-down list with names and values. + * Optionally include values for style and class */ { int i; char *selString; if (checked == NULL) checked = menu[0]; -if (NULL != extraAttribs) - { - printf("\n", name); + printf(" id='%s'", name); + jsOnEventById(event, name, javascript); } +if (style) + printf(" style='%s'", style); +printf(">\n"); for (i=0; i%s\n", selString, values[i], menu[i]); } printf("\n"); } +void cgiMakeDropListFull(char *name, char *menu[], char *values[], + int menuSize, char *checked, char *event, char *javascript) +/* Make a table of checkboxes that have the same variable name but different + * values (same behavior as a multi-select input). */ +{ +cgiMakeDropListFullExt(name, menu, values, menuSize, checked, event, javascript, NULL, NULL); +} + char *cgiMakeSelectDropList(boolean multiple, char *name, struct slPair *valsAndLabels, - char *selected, char *anyAll,char *extraClasses, char *extraHtml) + char *selected, char *anyAll,char *extraClasses, char *event, char *javascript, char *style, char *id) // Returns allocated string of HTML defining a drop-down select // (if multiple, REQUIRES ui-dropdownchecklist.js) // valsAndLabels: val (pair->name) must be filled in but label (pair->val) may be NULL. // selected: if not NULL is a val found in the valsAndLabels (multiple then comma delimited list). // If null and anyAll not NULL, that will be selected // anyAll: if not NULL is the string for an initial option. It can contain val and label, // delimited by a comma -// extraHtml: if not NULL contains id, javascript calls and style. -// It does NOT contain class definitions +// event: click, etc. +// javacript: what to execute when the event happens. +// style: inline style +// id is optional { struct dyString *output = dyStringNew(1024); boolean checked = FALSE; dyStringPrintf(output,"