From 07a8e4a177550025c1a08d7ac754839733943f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82=C4=99biowski-Owczarek?= Date: Tue, 25 Aug 2020 21:28:30 +0200 Subject: [PATCH] Ajax: Avoid CSP errors in the script transport for async requests Until now, the AJAX script transport only used a script tag to load scripts for cross-domain requests or ones with `scriptAttrs` set. This commit makes it also used for all async requests to avoid CSP errors arising from usage of inline scripts. This also makes `jQuery.getScript` not trigger CSP errors as it uses the AJAX script transport under the hood. For sync requests such a change is impossible and that's what `jQuery._evalUrl` uses. Fixing that is tracked in gh-1895. The commit also makes other type of requests using the script tag version of the script transport set its type to "GET", namely async scripts & ones with `scriptAttrs` set in addition to the existing cross-domain ones. Fixes gh-3969 Closes gh-4763 --- src/ajax/script.js | 10 +++++++--- test/data/csp-ajax-script-downloaded.js | 1 + test/data/csp-ajax-script.html | 13 +++++++++++++ test/data/csp-ajax-script.js | 25 +++++++++++++++++++++++++ test/data/mock.php | 10 ++++++---- test/middleware-mockserver.js | 9 +++++++++ test/unit/ajax.js | 22 ++++++++++++++++++++-- 7 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 test/data/csp-ajax-script-downloaded.js create mode 100644 test/data/csp-ajax-script.html create mode 100644 test/data/csp-ajax-script.js diff --git a/src/ajax/script.js b/src/ajax/script.js index 22dc29183..54bfecf2e 100644 --- a/src/ajax/script.js +++ b/src/ajax/script.js @@ -32,7 +32,10 @@ jQuery.ajaxPrefilter( "script", function( s ) { if ( s.cache === undefined ) { s.cache = false; } - if ( s.crossDomain ) { + + // These types of requests are handled via a script tag + // so force their methods to GET. + if ( s.crossDomain || s.async || s.scriptAttrs ) { s.type = "GET"; } } ); @@ -40,8 +43,9 @@ jQuery.ajaxPrefilter( "script", function( s ) { // Bind script tag hack transport jQuery.ajaxTransport( "script", function( s ) { - // This transport only deals with cross domain or forced-by-attrs requests - if ( s.crossDomain || s.scriptAttrs ) { + // This transport only deals with async, cross domain or forced-by-attrs requests. + // Sync requests remain handled differently to preserve strict script ordering. + if ( s.crossDomain || s.async || s.scriptAttrs ) { var script, callback; return { send: function( _, complete ) { diff --git a/test/data/csp-ajax-script-downloaded.js b/test/data/csp-ajax-script-downloaded.js new file mode 100644 index 000000000..4bd46cb65 --- /dev/null +++ b/test/data/csp-ajax-script-downloaded.js @@ -0,0 +1 @@ +window.downloadedScriptCalled = true; diff --git a/test/data/csp-ajax-script.html b/test/data/csp-ajax-script.html new file mode 100644 index 000000000..e3e750727 --- /dev/null +++ b/test/data/csp-ajax-script.html @@ -0,0 +1,13 @@ + + + + + jQuery.ajax() - script, CSP script-src compat (gh-3969) + + + + + +

CSP Test Page

+ + diff --git a/test/data/csp-ajax-script.js b/test/data/csp-ajax-script.js new file mode 100644 index 000000000..c6821a24e --- /dev/null +++ b/test/data/csp-ajax-script.js @@ -0,0 +1,25 @@ +/* global startIframeTest */ + +var timeoutId, type; + +function finalize() { + startIframeTest( type, window.downloadedScriptCalled ); +} + +timeoutId = setTimeout( function() { + finalize(); +}, 1000 ); + +jQuery + .ajax( { + url: "csp-ajax-script-downloaded.js", + dataType: "script", + method: "POST", + beforeSend: function( _jqXhr, settings ) { + type = settings.type; + } + } ) + .then( function() { + clearTimeout( timeoutId ); + finalize(); + } ); diff --git a/test/data/mock.php b/test/data/mock.php index a34e8f5ac..b76fd521c 100644 --- a/test/data/mock.php +++ b/test/data/mock.php @@ -195,22 +195,24 @@ QUnit.assert.ok( true, "mock executed");'; } protected function cspFrame( $req ) { - // This is CSP only for browsers with "Content-Security-Policy" header support - // i.e. no old WebKit or old Firefox header( "Content-Security-Policy: default-src 'self'; report-uri ./mock.php?action=cspLog" ); header( 'Content-type: text/html' ); echo file_get_contents( __DIR__ . '/csp.include.html' ); } protected function cspNonce( $req ) { - // This is CSP only for browsers with "Content-Security-Policy" header support - // i.e. no old WebKit or old Firefox $test = $req->query['test'] ? '-' . $req->query['test'] : ''; header( "Content-Security-Policy: script-src 'nonce-jquery+hardcoded+nonce'; report-uri ./mock.php?action=cspLog" ); header( 'Content-type: text/html' ); echo file_get_contents( __DIR__ . '/csp-nonce' . $test . '.html' ); } + protected function cspAjaxScript( $req ) { + header( "Content-Security-Policy: script-src 'self'; report-uri /base/test/data/mock.php?action=cspLog" ); + header( 'Content-type: text/html' ); + echo file_get_contents( __DIR__ . '/csp-ajax-script.html' ); + } + protected function cspLog( $req ) { file_put_contents( $this->cspFile, 'error' ); } diff --git a/test/middleware-mockserver.js b/test/middleware-mockserver.js index 39ccfb9f6..d0329eb9b 100644 --- a/test/middleware-mockserver.js +++ b/test/middleware-mockserver.js @@ -222,6 +222,15 @@ var mocks = { __dirname + "/data/csp-nonce" + testParam + ".html" ).toString(); resp.end( body ); }, + cspAjaxScript: function( req, resp ) { + resp.writeHead( 200, { + "Content-Type": "text/html", + "Content-Security-Policy": "script-src 'self'; report-uri /base/test/data/mock.php?action=cspLog" + } ); + var body = fs.readFileSync( + __dirname + "/data/csp-ajax-script.html" ).toString(); + resp.end( body ); + }, cspLog: function( req, resp ) { cspLog = "error"; resp.writeHead( 200 ); diff --git a/test/unit/ajax.js b/test/unit/ajax.js index cd7822b73..125470519 100644 --- a/test/unit/ajax.js +++ b/test/unit/ajax.js @@ -89,17 +89,21 @@ QUnit.module( "ajax", { } ); - ajaxTest( "jQuery.ajax() - custom attributes for script tag", 4, + ajaxTest( "jQuery.ajax() - custom attributes for script tag", 5, function( assert ) { return { create: function( options ) { var xhr; + options.method = "POST"; options.dataType = "script"; options.scriptAttrs = { id: "jquery-ajax-test", async: "async" }; xhr = jQuery.ajax( url( "mock.php?action=script" ), options ); assert.equal( jQuery( "#jquery-ajax-test" ).attr( "async" ), "async", "attr value" ); return xhr; }, + beforeSend: function( _jqXhr, settings ) { + assert.strictEqual( settings.type, "GET", "Type changed to GET" ); + }, success: function() { assert.ok( true, "success" ); }, @@ -1356,6 +1360,17 @@ QUnit.module( "ajax", { } ); + testIframe( + "jQuery.ajax() - script, CSP script-src compat (gh-3969)", + "mock.php?action=cspAjaxScript", + function( assert, jQuery, window, document, type, downloadedScriptCalled ) { + assert.expect( 2 ); + + assert.strictEqual( type, "GET", "Type changed to GET" ); + assert.strictEqual( downloadedScriptCalled, true, "External script called" ); + } + ); + ajaxTest( "jQuery.ajax() - script, Remote", 2, function( assert ) { return { setup: function() { @@ -1369,12 +1384,15 @@ QUnit.module( "ajax", { }; } ); - ajaxTest( "jQuery.ajax() - script, Remote with POST", 3, function( assert ) { + ajaxTest( "jQuery.ajax() - script, Remote with POST", 4, function( assert ) { return { setup: function() { Globals.register( "testBar" ); }, url: url( "mock.php?action=testbar" ), + beforeSend: function( _jqXhr, settings ) { + assert.strictEqual( settings.type, "GET", "Type changed to GET" ); + }, type: "POST", dataType: "script", success: function( data, status ) {