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/htmshell.c src/lib/htmshell.c
index c1fc4ae..6b9b3a2 100644
--- src/lib/htmshell.c
+++ src/lib/htmshell.c
@@ -540,92 +540,124 @@
char *htmlWarnEndPattern()
/* Return ending pattern for warning message. */
{
return "\n";
}
void htmlWarnBoxSetup(FILE *f)
/* Creates an invisible, empty warning box than can be filled with errors
* and then made visible. */
{
// Only set this up once per page
if (htmlWarnBoxSetUpAlready)
return;
htmlWarnBoxSetUpAlready=TRUE;
+// NOTE: There is a duplicate of this function in hg/js/utils.js
+
// NOTE: Making both IE and FF work is almost impossible. Currently, in IE, if the message
// is forced to the top (calling this routine after
then the box is not resizable
// (dynamically adjusting to its contents). But if this setup is done later in the page
// (at first warning), then IE does resize it. Why?
// FF3.0 (but not FF2.0) was resizable with the following, but it took some experimentation.
// Remember what worked nicely on FF3.0:
// "var app=navigator.appName.substr(0,9); "
// "if(app == 'Microsoft') {warnBox.style.display='';}
// else {warnBox.style.display=''; warnBox.style.width='auto';}"
-fprintf(f, "\n");
+// Added by Galt
+dyStringPrintf(dy,"document.getElementById('warnOK').onclick = function() {hideWarnBox();return false;};\n");
+//dyStringPrintf(dy,"$('#warnOK').click(function() {hideWarnBox();return false;});\n"); // jquery version
+dyStringPrintf(dy,"window.onunload = function(){}; // Trick to avoid FF back button issue.\n");
+
+jsInline(dy->string);
+dyStringFree(&dy);
}
void htmlVaWarn(char *format, va_list args)
/* Write an error message. */
{
va_list argscp;
va_copy(argscp, args);
htmlWarnBoxSetup(stdout); // sets up the warnBox if it hasn't already been done.
char warning[1024];
// html-encode arguments to fight XSS
int sz = vaHtmlSafefNoAbort(warning, sizeof(warning), format, args, TRUE, FALSE);
if (sz < 0)
{
safecpy(warning, sizeof(warning), "Low level error in htmlSafef. See error logs for details.");
}
// Replace newlines with BR tag
char *warningBR = htmlWarnEncode(warning);
// Javascript-encode the entire message because it is
// going to appear as a javascript string literal
// as it gets appended to the warnList html.
// JS-encoding here both allows us to use any character in the message
// and keeps js-encodings in events like onmouseover="stuff %s|js| stuff" secure.
char *jsEncodedMessage = javascriptEncode (warningBR);
freeMem(warningBR);
-printf("\n", jsEncodedMessage);
+ "warnList.innerHTML += '
%s
';\n", jsEncodedMessage);
+jsInline(dy->string);
+// TODO GALT does --ERROR -- tag still work?
+printf("\n");
// NOTE that "--ERROR --" is needed at the end of this print!!
+dyStringFree(&dy);
freeMem(jsEncodedMessage);
/* Log useful CGI info to stderr */
logCgiToStderr();
/* write warning/error message to stderr so they get logged. */
vfprintf(stderr, format, argscp);
fprintf(stderr, "\n");
fflush(stderr);
va_end(argscp);
}
void htmlVaBadRequestAbort(char *format, va_list args)
/* Print out an HTTP header 400 status code (Bad Request) and message,
* then exit with error. For use as an errAbort handler. */
@@ -795,51 +827,265 @@
char buf[FILENAME_LEN];
splitPath(scriptName, NULL, buf, NULL);
slNameAddHead(&classes, cloneString(buf));
}
if (htmlFormClass != NULL )
slNameAddHead(&classes, htmlFormClass);
fprintf(f, " CLASS=\"%s\"", slNameListToString(classes, ' '));
if (htmlBackground != NULL )
fprintf(f, " BACKGROUND=\"%s\"", htmlBackground);
if (gotBgColor)
fprintf(f, " BGCOLOR=\"#%X\"", htmlBgColor);
fputs(">\n",f);
}
+//--- NONCE and CSP routines -------------
+
+#include "base64.h"
+// copied from cartDb::cartDbMakeRandomKey()
+char *makeRandomKey(int numBits)
+/* Generate base64 encoding of a random key of at least size numBits returning string to be freed when done */
+{
+int numBytes = (numBits + 7) / 8; // round up to nearest whole byte.
+numBytes = ((numBytes+2)/3)*3; // round up to the nearest multiple of 3 to avoid equals-char padding in base64 output
+FILE *f = mustOpen("/dev/urandom", "r"); // open random system device for read-only access.
+char *binaryString = needMem(numBytes);
+mustRead(f, binaryString, numBytes);
+carefulClose(&f);
+char * result = base64Encode(binaryString, numBytes); // converts 3 binary bytes into 4 printable characters
+int len = strlen(result);
+memSwapChar(result, len, '+', 'A'); // replace + and / with characters that are URL-friendly.
+memSwapChar(result, len, '/', 'a');
+freeMem(binaryString);
+return result;
+}
+
+char *nonce = NULL;
+
+char *getNonce()
+/* make nonce one-use-per-page */
+{
+if (!nonce)
+ {
+ nonce = makeRandomKey(128+33); // at least 128 bits of protection, 33 for the world population size.
+ }
+return nonce;
+}
+
+char *getNoncePolicy()
+/* get nonce policy clause */
+{
+char nonceClause[1024];
+safef(nonceClause, sizeof nonceClause, "'nonce-%s'", getNonce());
+return cloneString(nonceClause);
+}
+
+/* CSP2 Usage Notes
+One can add js scripts to a DOM dynamically.
+It is critical to use script.setAttribute('nonce', pageNonce);
+instead of script.nonce = pageNonce; because nonce is an html attribute,
+not a javascript attribute. In places where this is used (like alleles.js)
+we should mark this with CSP2 comment because someday CSP3 will automatically
+be able to pass the nonce to script children and the command will no longer be
+needed.
+ */
+
+/* CSP3 Usage Notes
+(NOT IN USE YET).
+We almost went with CSP3 even though it is very new at this time (2017-01-26).
+It has one important new script-src directive 'strict-dynamic' which does 3
+things:
+
+First, the word "strict" means that it only uses nonces -- the whitelist of sites and paths
+is completely ignored. When we adopt it someday we will need to go through
+our source code and add the nonce='random' to our non-inline js script includes.
+There are probably only a few dozen places in the code that do this and it should
+be easy. The reason that whitelists can be a big problem is that they are brittle,
+can grow to be very large and unweildy and hard to maintain. Furthermore, an
+entire large portal often has other stuff there which hackers can exploit and which
+you were not intending to authorize. If the whitelisted site has jsonp endpoints,
+redirection-scripts, AugustusJS, and other fancy js frameworks, or even old jquery includes with bugs,
+then there are exploits that whitelists do not protect against.
+
+Second, the word "dynamic" in the directive means that it dynamically delegates trust authorization
+to other scripts, so that whatever stuff libraries include are automaically trusted recursively.
+Non-inline script libraries, both local and off-site will require nonces alike.
+
+Third, the final thing that 'strict-dynamic' does is that
+when a nonced-script dynamically adds a script to the DOM dynamically with createElement('script')
+and document.head.appendChild(script);, the nonce value is added automatically,
+unlike CSP2. 'script-dynamic' specifically encourages such DOM dynamic additions
+while discouraging more dangerous methods which require raw html string parsing followed by eval such as
+innerHTML and document.write.
+
+These changes are aimed at making CSP more user-friendly and deployable.
+
+So to deploy CSP3 is fairly easy from a code-change point of view. Just add 'strict-dynamic'
+to the CSP policy and add some nonces to the js library include lines. But we do not
+accrue that much value from it over CSP2 at this time. Plus, because there is currently
+no easy way to tell which CSP level a browser supports, and we have no huge whitelist at this time,
+there is not much benefit. Also with CSP2, they gave us the trick of using 'unsafe-inline' together
+with 'nonce=random' so that CSP1 would have no enforcement, but would at least run without errors
+and allow inline js, while CSP2 DOES have nonce-protected inline js. BUT They did not create
+any equivalent policy for backwards compatibility that would likewise disable enforcement in CSP2 browsers
+while enabling full enforcement in CSP3 browsers. Without that trick we are basically stuck
+with a CSP2 level policy until basically nearly all browsers have CSP3. This could take several years,
+even though Chrome and FF already support CSP3 and even MS Edge will have it soon.
+I contacted the designers of 'strict-dynamic', but it was too late to change CSP3.
+*/
+
+
+char *getCspPolicyString()
+/* get the policy string */
+{
+// example "default-src 'self'; child-src 'none'; object-src 'none'"
+struct dyString *policy = dyStringNew(1024);
+dyStringAppend(policy, "default-src *;");
+
+/* more secure method not used yet
+dyStringAppend(policy, "default-src 'self';");
+
+dyStringAppend(policy, " child-src 'self';");
+*/
+
+dyStringAppend(policy, " script-src 'self'");
+// Trick for backwards compatibility with browsers that understand CSP1 but not nonces (CSP2).
+dyStringAppend(policy, " 'unsafe-inline'");
+// For browsers that DO understand nonces and CSP2, they ignore 'unsafe-inline' in script if nonce is present.
+char *noncePolicy=getNoncePolicy();
+dyStringPrintf(policy, " %s", noncePolicy);
+freeMem(noncePolicy);
+dyStringAppend(policy, " code.jquery.com"); // used by hgIntegrator jsHelper and others
+dyStringAppend(policy, " www.google-analytics.com"); // used by google analytics
+// cirm cdw lib and web browse
+dyStringAppend(policy, " cpettitt.github.io/project/dagre-d3/latest/dagre-d3.js");
+dyStringAppend(policy, " cdnjs.cloudflare.com/ajax/libs/d3/3.4.4/d3.min.js");
+dyStringAppend(policy, " login.persona.org/include.js");
+// expMatrix
+dyStringAppend(policy, " ajax.googleapis.com/ajax");
+dyStringAppend(policy, " maxcdn.bootstrapcdn.com/bootstrap");
+dyStringAppend(policy, " d3js.org/d3.v3.min.js");
+// jsHelper
+dyStringAppend(policy, " cdn.datatables.net");
+
+dyStringAppend(policy, ";");
+
+
+dyStringAppend(policy, " style-src * 'unsafe-inline';");
+
+/* more secure method not used yet
+dyStringAppend(policy, " style-src 'self' 'unsafe-inline'");
+dyStringAppend(policy, " code.jquery.com"); // used by hgIntegrator
+dyStringAppend(policy, " netdna.bootstrapcdn.com"); // used by hgIntegrator
+dyStringAppend(policy, " fonts.googleapis.com"); // used by hgGateway
+dyStringAppend(policy, " maxcdn.bootstrapcdn.com"); // used by hgGateway
+dyStringAppend(policy, ";");
+*/
+
+// The data: protocol is used by popular browser extensions.
+// It seems to be safe and it is too bad that it must be explicitly included.
+dyStringAppend(policy, " font-src * data:;");
+
+/* more secure method not used yet
+dyStringAppend(policy, " font-src 'self'");
+dyStringAppend(policy, " netdna.bootstrapcdn.com"); // used by hgIntegrator
+dyStringAppend(policy, " maxcdn.bootstrapcdn.com"); // used by hgGateway
+dyStringAppend(policy, " fonts.gstatic.com"); // used by hgGateway
+dyStringAppend(policy, ";");
+
+dyStringAppend(policy, " object-src 'none';");
+
+*/
+
+dyStringAppend(policy, " img-src * data:;");
+
+/* more secure method not used yet
+dyStringAppend(policy, " img-src 'self'");
+// used by hgGene for modbaseimages in hg/hgc/lowelab.c hg/protein/lib/domains.c hg/hgGene/domains.c
+dyStringAppend(policy, " modbase.compbio.ucsf.edu");
+dyStringAppend(policy, " hgwdev.cse.ucsc.edu"); // used by visiGene
+dyStringAppend(policy, " genome.ucsc.edu"); // used by visiGene
+dyStringAppend(policy, " code.jquery.com"); // used by hgIntegrator
+dyStringAppend(policy, " www.google-analytics.com"); // used by google analytics
+dyStringAppend(policy, " stats.g.doubleclick.net"); // used by google analytics
+dyStringAppend(policy, ";");
+*/
+return dyStringCannibalize(&policy);
+}
+
+char *getCspMetaString(char *policy)
+/* get the policy string as an html header meta tag */
+{
+char meta[1024];
+safef(meta, sizeof meta, "\n", policy);
+// use double quotes around policy because it contains single-quoted values.
+return cloneString(meta);
+}
+
+char *getCspMetaResponseHeader(char *policy)
+/* get the policy string as an html response header */
+{
+char response[4096];
+safef(response, sizeof response, "Content-Security-Policy: %s\n", policy);
+return cloneString(response);
+}
+
+char *getCspMetaHeader()
+/* return meta CSP header string */
+{
+char *policy = getCspPolicyString();
+char *meta = getCspMetaString(policy);
+freeMem(policy);
+return meta;
+}
+
+void generateCspMetaHeader(FILE *f)
+/* generate meta CSP header */
+{
+char *meta = getCspMetaHeader();
+fputs(meta, f);
+freeMem(meta);
+}
+
+
void _htmStartWithHead(FILE *f, char *head, char *title, boolean printDocType, int dirDepth)
/* Write out bits of header that both stand-alone .htmls
* and CGI returned .htmls need, including optional head info */
{
if (printDocType)
{
//#define TOO_TIMID_FOR_CURRENT_HTML_STANDARDS
#ifdef TOO_TIMID_FOR_CURRENT_HTML_STANDARDS
fputs("\n", f);
#else///ifndef TOO_TIMID_FOR_CURRENT_HTML_STANDARDS
char *browserVersion;
if (btIE == cgiClientBrowser(&browserVersion, NULL, NULL) && *browserVersion < '8')
fputs("\n", f);
else
fputs("\n",f);
// Strict would be nice since it fixes atleast one IE problem (use of :hover CSS pseudoclass)
#endif///ndef TOO_TIMID_FOR_CURRENT_HTML_STANDARDS
}
-fputs("", f);
-htmlFprintf(f,"\n%s|none|%s\n", head, title); // TODO "head" var. not XSS safe
+fputs("\n", f);
+fputs("\n", f);
+// CSP header
+generateCspMetaHeader(f);
+
+fputs(head, f); // TODO "head" var. not XSS safe
+htmlFprintf(f,"%s\n", title);
if (endsWith(title,"Login - UCSC Genome Browser"))
fprintf(f,"\t\n");
fprintf(f, "\t\n");
if (htmlStyle != NULL)
fputs(htmlStyle, f);
if (htmlStyleSheet != NULL)
fprintf(f,"\n", htmlStyleSheet);
if (htmlStyleTheme != NULL)
fputs(htmlStyleTheme, f);
fputs("\n\n",f);
printBodyTag(f);
htmlWarnBoxSetup(f);
}
@@ -863,30 +1109,32 @@
{
htmStartWithHead(f, "", title);
}
void htmStartDirDepth(FILE *f, char *title, int dirDepth)
/* Write the start of a stand alone .html file. dirDepth is the number of levels
* beneath apache root that caller's HTML will appear to the web client.
* E.g. if writing HTML from cgi-bin, dirDepth is 1; if trash/body/, 2. */
{
_htmStartWithHead(f, "", title, TRUE, dirDepth);
}
/* Write the end of an html file */
void htmEnd(FILE *f)
{
+if (f == stdout) // not html for a frame of a frameset
+ jsInlineFinish();
fputs("\n\n\n", f);
}
/* Write the end of a stand-alone html file */
void htmlEnd()
{
htmEnd(stdout);
}
void htmlBadVar(char *varName)
{
cgiBadVar(varName);
}
/* Display centered image file. */