Bug 600584 - add more detail to CSP violation report logging, r=jst, a=LegNeato
authorBrandon Sterne <bsterne@mozilla.com>
Mon, 31 Jan 2011 10:09:44 -0800
changeset 61654 c71dc0ee1973787687100c3b294238c775a85f9a
parent 61653 2e64d11f6885dc70aa08e6fbe10de1f161d8ceb6
child 61655 0162ac678b8ff5e7278275243758835e3c072679
push idunknown
push userunknown
push dateunknown
reviewersjst, LegNeato
bugs600584
milestone2.0b11pre
Bug 600584 - add more detail to CSP violation report logging, r=jst, a=LegNeato
caps/src/nsScriptSecurityManager.cpp
content/base/public/nsIContentSecurityPolicy.idl
content/base/src/CSPUtils.jsm
content/base/src/contentSecurityPolicy.js
content/base/src/nsScriptLoader.cpp
content/events/src/nsEventListenerManager.cpp
dom/src/jsurl/nsJSProtocolHandler.cpp
--- a/caps/src/nsScriptSecurityManager.cpp
+++ b/caps/src/nsScriptSecurityManager.cpp
@@ -547,26 +547,53 @@ nsScriptSecurityManager::ContentSecurity
     rv = subjectPrincipal->GetCsp(getter_AddRefs(csp));
     NS_ASSERTION(NS_SUCCEEDED(rv), "CSP: Failed to get CSP from principal.");
 
     // don't do anything unless there's a CSP
     if (!csp)
         return JS_TRUE;
 
     PRBool evalOK = PR_TRUE;
-    // this call will send violation reports as warranted (and return true if
-    // reportOnly is set).
     rv = csp->GetAllowsEval(&evalOK);
 
     if (NS_FAILED(rv))
     {
         NS_WARNING("CSP: failed to get allowsEval");
         return JS_TRUE; // fail open to not break sites.
     }
 
+    if (!evalOK) {
+        // get the script filename, script sample, and line number
+        // to log with the violation
+        JSStackFrame *fp = nsnull;
+        nsAutoString fileName;
+        PRUint32 lineNum = 0;
+        NS_NAMED_LITERAL_STRING(scriptSample, "call to eval() or related function blocked by CSP");
+
+        fp = JS_FrameIterator(cx, &fp);
+        if (fp) {
+            JSScript *script = JS_GetFrameScript(cx, fp);
+            if (script) {
+                const char *file = JS_GetScriptFilename(cx, script);
+                if (file) {
+                    CopyUTF8toUTF16(nsDependentCString(file), fileName);
+                }
+                jsbytecode *pc = JS_GetFramePC(cx, fp);
+                if (pc) {
+                    lineNum = JS_PCToLineNumber(cx, script, pc);
+                }
+            }
+        }
+
+        csp->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL,
+                                 fileName,
+                                 scriptSample,
+                                 lineNum);
+    }
+
     return evalOK;
 }
 
 
 JSBool
 nsScriptSecurityManager::CheckObjectAccess(JSContext *cx, JSObject *obj,
                                            jsid id, JSAccessMode mode,
                                            jsval *vp)
--- a/content/base/public/nsIContentSecurityPolicy.idl
+++ b/content/base/public/nsIContentSecurityPolicy.idl
@@ -65,42 +65,64 @@ interface nsIContentSecurityPolicy : nsI
 
   /**
    * A read-only string version of the policy for debugging.
    */
   readonly attribute AString policy;
 
   /**
    * Whether this policy allows in-page script.
-   *
-   * Calls to this may trigger violation reports when queried, so
-   * this value should not be cached.
    */
   readonly attribute boolean allowsInlineScript;
 
   /**
    * whether this policy allows eval and eval-like functions
    * such as setTimeout("code string", time).
-   *
-   * Calls to this may trigger violation reports when queried, so
-   * this value should not be cached.
    */
   readonly attribute boolean allowsEval;
 
   /**
+   * Log policy violation on the Error Console and send a report if a report-uri
+   * is present in the policy
+   *
+   * @param violationType
+   *     one of the VIOLATION_TYPE_* constants, e.g. inline-script or eval
+   * @param sourceFile
+   *     name of the source file containing the violation (if available)
+   * @param contentSample
+   *     sample of the violating content (to aid debugging)
+   * @param lineNum
+   *     source line number of the violation (if available)
+   */
+  void logViolationDetails(in unsigned short violationType,
+                           in AString sourceFile,
+                           in AString scriptSample,
+                           in PRInt32 lineNum);
+
+  const unsigned short VIOLATION_TYPE_INLINE_SCRIPT = 1;
+  const unsigned short VIOLATION_TYPE_EVAL = 2;
+
+  /**
    * Manually triggers violation report sending given a URI and reason.
    * The URI may be null, in which case "self" is sent.
    * @param blockedURI
    *     the URI that violated the policy
    * @param violatedDirective
    *     the directive that was violated.
+   * @param scriptSample
+   *     a sample of the violating inline script
+   * @param lineNum
+   *     source line number of the violation (if available)
    * @return 
    *     nothing.
    */
-  void sendReports(in AString blockedURI, in AString violatedDirective);
+  void sendReports(in AString blockedURI,
+                   in AString violatedDirective,
+                   in AString scriptSample,
+                   in PRInt32 lineNum);
 
   /**
    * Called after the CSP object is created to fill in the appropriate request
    * and request header information needed in case a report needs to be sent.
    */
   void scanRequestData(in nsIHttpChannel aChannel);
 
   /**
--- a/content/base/src/CSPUtils.jsm
+++ b/content/base/src/CSPUtils.jsm
@@ -78,22 +78,22 @@ var gPrefObserver = {
     if(aTopic != "nsPref:changed") return;
     if(aData === "debug")
       this._debugEnabled = this._branch.getBoolPref("debug");
   },
 
 };
 
 
-function CSPWarning(aMsg) {
+function CSPWarning(aMsg, aSource, aScriptSample, aLineNum) {
   var textMessage = 'CSP WARN:  ' + aMsg + "\n";
 
   var consoleMsg = Components.classes["@mozilla.org/scripterror;1"]
                     .createInstance(Components.interfaces.nsIScriptError);
-  consoleMsg.init('CSP: ' + aMsg, null, null, 0, 0,
+  consoleMsg.init('CSP: ' + aMsg, aSource, aScriptSample, aLineNum, 0,
                   Components.interfaces.nsIScriptError.warningFlag,
                   "Content Security Policy");
   Components.classes["@mozilla.org/consoleservice;1"]
                     .getService(Components.interfaces.nsIConsoleService)
                     .logMessage(consoleMsg);
 }
 
 function CSPError(aMsg) {
--- a/content/base/src/contentSecurityPolicy.js
+++ b/content/base/src/contentSecurityPolicy.js
@@ -122,31 +122,55 @@ ContentSecurityPolicy.prototype = {
     this._isInitialized = foo;
   },
 
   get policy () {
     return this._policy.toString();
   },
 
   get allowsInlineScript() {
-    // trigger automatic report to go out when inline scripts are disabled.
-    if (!this._policy.allowsInlineScripts) {
-      this._asyncReportViolation('self','inline script base restriction',
-                                 'violated base restriction: Inline Scripts will not execute');
-    }
     return this._reportOnlyMode || this._policy.allowsInlineScripts;
   },
 
   get allowsEval() {
-    // trigger automatic report to go out when eval and friends are disabled.
-    if (!this._policy.allowsEvalInScripts) {
-      this._asyncReportViolation('self','eval script base restriction',
-                                 'violated base restriction: Code will not be created from strings');
+    return this._reportOnlyMode || this._policy.allowsEvalInScripts;
+  },
+
+  /**
+   * Log policy violation on the Error Console and send a report if a report-uri
+   * is present in the policy
+   *
+   * @param aViolationType
+   *     one of the VIOLATION_TYPE_* constants, e.g. inline-script or eval
+   * @param aSourceFile
+   *     name of the source file containing the violation (if available)
+   * @param aContentSample
+   *     sample of the violating content (to aid debugging)
+   * @param aLineNum
+   *     source line number of the violation (if available)
+   */
+  logViolationDetails:
+  function(aViolationType, aSourceFile, aScriptSample, aLineNum) {
+    // allowsInlineScript and allowsEval both return true when report-only mode
+    // is enabled, resulting in a call to this function. Therefore we need to
+    // check that the policy was in fact violated before logging any violations
+    switch (aViolationType) {
+    case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_INLINE_SCRIPT:
+      if (!this._policy.allowsInlineScripts)
+        this._asyncReportViolation('self','inline script base restriction',
+                                   'violated base restriction: Inline Scripts will not execute',
+                                   aSourceFile, aScriptSample, aLineNum);
+      break;
+    case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_EVAL:
+      if (!this._policy.allowsEvalInScripts)
+        this._asyncReportViolation('self','eval script base restriction',
+                                   'violated base restriction: Code will not be created from strings',
+                                   aSourceFile, aScriptSample, aLineNum);
+      break;
     }
-    return this._reportOnlyMode || this._policy.allowsEvalInScripts;
   },
 
   set reportOnlyMode(val) {
     this._reportOnlyMode = val;
   },
 
   get reportOnlyMode () {
     return this._reportOnlyMode;
@@ -229,17 +253,17 @@ ContentSecurityPolicy.prototype = {
     this._policy = intersect;
     this._isInitialized = true;
   },
 
   /**
    * Generates and sends a violation report to the specified report URIs.
    */
   sendReports:
-  function(blockedUri, violatedDirective) {
+  function(blockedUri, violatedDirective, aSourceFile, aScriptSample, aLineNum) {
     var uriString = this._policy.getReportURIs();
     var uris = uriString.split(/\s+/);
     if (uris.length > 0) {
       // Generate report to send composed of
       // {
       //   csp-report: {
       //     request: "GET /index.html HTTP/1.1",
       //     request-headers: "Host: example.com
@@ -257,20 +281,31 @@ ContentSecurityPolicy.prototype = {
         'csp-report': {
           'request': this._request,
           'request-headers': strHeaders,
           'blocked-uri': (blockedUri instanceof Ci.nsIURI ?
                           blockedUri.asciiSpec : blockedUri),
           'violated-directive': violatedDirective
         }
       }
+      // extra report fields for script errors (if available)
+      if (aSourceFile)
+        report["csp-report"]["source-file"] = aSourceFile;
+      if (aScriptSample)
+        report["csp-report"]["script-sample"] = aScriptSample;
+      if (aLineNum)
+        report["csp-report"]["line-number"] = aLineNum;
+
       CSPdebug("Constructed violation report:\n" + JSON.stringify(report));
 
       CSPWarning("Directive \"" + violatedDirective + "\" violated"
-               + (blockedUri['asciiSpec'] ? " by " + blockedUri.asciiSpec : ""));
+               + (blockedUri['asciiSpec'] ? " by " + blockedUri.asciiSpec : ""),
+                 (aSourceFile) ? aSourceFile : null,
+                 (aScriptSample) ? decodeURIComponent(aScriptSample) : null,
+                 (aLineNum) ? aLineNum : null);
 
       // For each URI in the report list, send out a report.
       // We make the assumption that all of the URIs are absolute URIs; this
       // should be taken care of in CSPRep.fromString (where it converts any
       // relative URIs into absolute ones based on "self").
       for (let i in uris) {
         if (uris[i] === "")
           continue;
@@ -434,19 +469,26 @@ ContentSecurityPolicy.prototype = {
    * @param blockedContentSource
    *        Either a CSP Source (like 'self', as string) or nsIURI: the source
    *        of the violation.
    * @param violatedDirective
    *        the directive that was violated (string).
    * @param observerSubject
    *        optional, subject sent to the nsIObservers listening to the CSP
    *        violation topic.
+   * @param aSourceFile
+   *        name of the file containing the inline script violation
+   * @param aScriptSample
+   *        a sample of the violating inline script
+   * @param aLineNum
+   *        source line number of the violation (if available)
    */
   _asyncReportViolation:
-  function(blockedContentSource, violatedDirective, observerSubject) {
+  function(blockedContentSource, violatedDirective, observerSubject,
+           aSourceFile, aScriptSample, aLineNum) {
     // if optional observerSubject isn't specified, default to the source of
     // the violation.
     if (!observerSubject)
       observerSubject = blockedContentSource;
 
     // gotta wrap things that aren't nsISupports, since it's sent out to
     // observers as such.  Objects that are not nsISupports are converted to
     // strings and then wrapped into a nsISupportsCString.
@@ -458,14 +500,15 @@ ContentSecurityPolicy.prototype = {
     }
 
     var reportSender = this;
     Services.tm.mainThread.dispatch(
       function() {
         Services.obs.notifyObservers(observerSubject,
                                      CSP_VIOLATION_TOPIC,
                                      violatedDirective);
-        reportSender.sendReports(blockedContentSource, violatedDirective);
+        reportSender.sendReports(blockedContentSource, violatedDirective,
+                                 aSourceFile, aScriptSample, aLineNum);
       }, Ci.nsIThread.DISPATCH_NORMAL);
   },
 };
 
 var NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentSecurityPolicy]);
--- a/content/base/src/nsScriptLoader.cpp
+++ b/content/base/src/nsScriptLoader.cpp
@@ -669,22 +669,38 @@ nsScriptLoader::ProcessScriptElement(nsI
   // inline script
   nsCOMPtr<nsIContentSecurityPolicy> csp;
   rv = mDocument->NodePrincipal()->GetCsp(getter_AddRefs(csp));
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (csp) {
     PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("New ScriptLoader i ****with CSP****"));
     PRBool inlineOK;
-    // this call will send violation reports when necessary
     rv = csp->GetAllowsInlineScript(&inlineOK);
     NS_ENSURE_SUCCESS(rv, rv);
 
     if (!inlineOK) {
       PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("CSP blocked inline scripts (2)"));
+      // gather information to log with violation report
+      nsIURI* uri = mDocument->GetDocumentURI();
+      nsCAutoString asciiSpec;
+      uri->GetAsciiSpec(asciiSpec);
+      nsAutoString scriptText;
+      aElement->GetScriptText(scriptText);
+
+      // cap the length of the script sample at 40 chars
+      if (scriptText.Length() > 40) {
+        scriptText.Truncate(40);
+        scriptText.Append(NS_LITERAL_STRING("..."));
+      }
+
+      csp->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_INLINE_SCRIPT,
+                               NS_ConvertUTF8toUTF16(asciiSpec),
+                               scriptText,
+                               aElement->GetScriptLineNumber());
       return NS_ERROR_FAILURE;
     }
   }
 
   request = new nsScriptLoadRequest(aElement, version);
   NS_ENSURE_TRUE(request, NS_ERROR_OUT_OF_MEMORY);
   request->mJSVersion = version;
   request->mLoading = PR_FALSE;
--- a/content/events/src/nsEventListenerManager.cpp
+++ b/content/events/src/nsEventListenerManager.cpp
@@ -729,26 +729,39 @@ nsEventListenerManager::AddScriptEventLi
   // 'doc' is fetched above
   if (doc) {
     nsCOMPtr<nsIContentSecurityPolicy> csp;
     rv = doc->NodePrincipal()->GetCsp(getter_AddRefs(csp));
     NS_ENSURE_SUCCESS(rv, rv);
 
     if (csp) {
       PRBool inlineOK;
-      // this call will trigger violaton reports if necessary
       rv = csp->GetAllowsInlineScript(&inlineOK);
       NS_ENSURE_SUCCESS(rv, rv);
 
       if ( !inlineOK ) {
-        //can log something here too.
-        //nsAutoString attr;
-        //aName->ToString(attr);
-        //printf(" *** CSP bailing on adding event listener for: %s\n",
-        //       ToNewCString(attr));
+        // gather information to log with violation report
+        nsIURI* uri = doc->GetDocumentURI();
+        nsCAutoString asciiSpec;
+        if (uri)
+          uri->GetAsciiSpec(asciiSpec);
+        nsAutoString scriptSample, attr, tagName(NS_LITERAL_STRING("UNKNOWN"));
+        aName->ToString(attr);
+        nsCOMPtr<nsIDOMNode> domNode(do_QueryInterface(aObject));
+        if (domNode)
+          domNode->GetNodeName(tagName);
+        // build a "script sample" based on what we know about this element
+        scriptSample.Assign(attr);
+        scriptSample.AppendLiteral(" attribute on ");
+        scriptSample.Append(tagName);
+        scriptSample.AppendLiteral(" element");
+        csp->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_INLINE_SCRIPT,
+                                 NS_ConvertUTF8toUTF16(asciiSpec),
+                                 scriptSample,
+                                 nsnull);
         return NS_OK;
       }
     }
   }
 
   // This might be the first reference to this language in the global
   // We must init the language before we attempt to fetch its context.
   if (NS_FAILED(global->EnsureScriptEnvironment(aLanguage))) {
--- a/dom/src/jsurl/nsJSProtocolHandler.cpp
+++ b/dom/src/jsurl/nsJSProtocolHandler.cpp
@@ -192,26 +192,33 @@ nsresult nsJSThunk::EvaluateScript(nsICh
 
     nsresult rv;
 
     // CSP check: javascript: URIs disabled unless "inline" scripts are
     // allowed.
     nsCOMPtr<nsIContentSecurityPolicy> csp;
     rv = principal->GetCsp(getter_AddRefs(csp));
     NS_ENSURE_SUCCESS(rv, rv);
-    if(csp) {
-      PRBool allowsInline;
-      // this call will send violation reports as warranted (and return true if
-      // reportOnly is set).
-      rv = csp->GetAllowsInlineScript(&allowsInline);
-      NS_ENSURE_SUCCESS(rv, rv);
+    if (csp) {
+		PRBool allowsInline;
+		rv = csp->GetAllowsInlineScript(&allowsInline);
+		NS_ENSURE_SUCCESS(rv, rv);
 
-      // TODO: log that we're blocking this javascript: uri
-      if (!allowsInline)
-        return NS_ERROR_DOM_RETVAL_UNDEFINED;
+      if (!allowsInline) {
+          // gather information to log with violation report
+          nsCOMPtr<nsIURI> uri;
+          principal->GetURI(getter_AddRefs(uri));
+          nsCAutoString asciiSpec;
+          uri->GetAsciiSpec(asciiSpec);
+		  csp->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_INLINE_SCRIPT,
+								   NS_ConvertUTF8toUTF16(asciiSpec),
+								   NS_ConvertUTF8toUTF16(mURL),
+                                   nsnull);
+          return NS_ERROR_DOM_RETVAL_UNDEFINED;
+      }
     }
 
     // Get the global object we should be running on.
     nsIScriptGlobalObject* global = GetGlobalObject(aChannel);
     if (!global) {
         return NS_ERROR_FAILURE;
     }