Third-Party Scripts Versus a Strict CSP

A strictly configured Content Security Policy (CSP) with a locked-down script-src whitelist, nonce-based sources, and mitigations against unsafe-eval is a strong defense against cross-site scripting. But a strict policy creates friction when a page needs to pull in a third-party JavaScript library. Consider a typical integration snippet for a chat widget:

var se = document.createElement('script');
se.type = 'text/javascript';
se.async = true;
se.src = '//storage.googleapis.com/code.snapengage.com/js/' + chatId + '.js';
se.onload = se.onreadystatechange = function() { ... //elided 
var s = document.getElementsByTagName('script')[0]; 
s.parentNode.insertBefore(se, s);

This code creates a script node pointing to the vendor's library and inserts it into the page. The loaded library then generates the widget's markup and event handlers on its own. Two problems arise from this pattern.

First, the widget's dynamically inserted markup and inline event handlers are not CSP-aware. Depending on how the library uses eval and inline handlers, the policy will break the widget. In practice, this forces you to loosen or disable CSP on pages that depend on the widget, which negates the security benefit. Dropbox hit this exact issue when rolling out nonces and had to disable them on pages using the SnapEngage widget.

Second, and more subtle, is the trust issue. All code running on the same origin as your application shares the same privileges. When you load the SnapEngage library directly into a Dropbox page, you are implicitly extending your trusted computing base to include SnapEngage's servers. Even with thorough vendor reviews and mandatory security requirements, the risk of a third-party compromise is real. Proposed web platform features like per-page suborigins could ultimately mitigate this, but they remain under discussion at the W3C.

Isolating the Widget in an Unprivileged Origin

An alternative to inlining third-party code is to host it in an iframe served from a separate, unprivileged origin. The trusted application origin then communicates with that iframe via postMessage. This is the same privilege separation model used by OpenSSH and Chrome, adapted for the web: the unprivileged component runs in its own origin, and a narrow, explicitly defined API bridges the two via messages instead of IPC.

For the SnapEngage integration, Dropbox serves an iframe from https://www.dbxsnapengage.com. Code on www.dropbox.com creates the iframe when a page that needs the chat widget loads. The iframe's own code then loads the SnapEngage library using the vendor's standard snippet. With CSS hiding the iframe's borders, the chat widget renders identically to how it would have if injected directly.

This setup preserves the visual and functional experience, but requires reworking the JavaScript events that previously connected the widget to the page. In the original approach, clicking the "Chat" button directly invoked a function like startSupportChat:

function startSupportChat() {
    SnapEngage.setWidgetId(SUPPORT_ID);
    SnapEngage.setUserEmail(chatData.Email, true)
    SnapEngage.startChat("How can we help you today?")
}

That function no longer exists on the privileged origin. Instead, the click handler on www.dropbox.com sends a postMessage to the iframe:

DropboxSnapEngage.startSupportChat = function() {
    this.chatRequested = true;
    DropboxSnapEngage.showSnapEngageIframe();
    return DropboxSnapEngage.sendMessage({
        'message_type': 'startSupportChat',
        'chatData': this.chatData
    });
};
 
DropboxSnapEngage.sendMessage = function(data) {
    var content_window;
    content_window = DropboxSnapEngage.getSnapEngageIframe().contentWindow;
    return content_window.postMessage(data, this.SNAPENGAGE_IFRAME_ORIGIN);
};

On the dbxsnapengage.com side, an event listener receives that message and calls the startSupportChat function in the context of the unprivileged page:

function receiveMessage(event) {
     if (!validOriginURL(event.origin)) return;
 
     var data = event.data
     switch (data.message_type) {
//elided ...
         case "startSupportChat":
             startSupportChat();
             break;
//elided ...
     }
 }

The result is that clicking the chat button still initiates a chat, but the widget and all of its code now execute entirely within the dbxsnapengage.com realm.

Practical Upsides

The iframe-isolation approach yields two concrete benefits. Third-party providers do not need to alter their code or conform to your CSP rules; they can continue using inline handlers and other patterns that would be disallowed on your primary origin. And because Dropbox owns and operates the separate domain, it fully controls the postMessage API crossing the trust boundary. Dropbox applies the same pattern elsewhere, including with its payments provider, to contain the risk from third-party integrations without crippling the functionality they provide.