Bug 1765289 - Early Hints: Parse additional header fields for content-policy-security r=necko-reviewers,kershaw,ckerschb,asuth

We capture the CSP header if present and gate the preload on the application of the policy.
A temporary loadInfo, csp, and clientInfo are created as no document yet exists.

Differential Revision: https://phabricator.services.mozilla.com/D162138
This commit is contained in:
Andrew Creskey 2023-01-08 19:40:44 +00:00
parent 64c5787d70
commit 11e3473f12
19 changed files with 617 additions and 72 deletions

View File

@ -2920,11 +2920,12 @@ NS_IMETHODIMP DocumentLoadListener::OnStatus(nsIRequest* aRequest,
return NS_OK;
}
NS_IMETHODIMP DocumentLoadListener::EarlyHint(
const nsACString& aLinkHeader, const nsACString& aReferrerPolicy) {
NS_IMETHODIMP DocumentLoadListener::EarlyHint(const nsACString& aLinkHeader,
const nsACString& aReferrerPolicy,
const nsACString& aCSPHeader) {
LOG(("DocumentLoadListener::EarlyHint.\n"));
mEarlyHintsService.EarlyHint(aLinkHeader, GetChannelCreationURI(), mChannel,
aReferrerPolicy);
aReferrerPolicy, aCSPHeader);
return NS_OK;
}

View File

@ -12,8 +12,11 @@
#include "NeckoCommon.h"
#include "mozilla/CORSMode.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/nsCSPContext.h"
#include "mozilla/dom/ReferrerInfo.h"
#include "mozilla/glean/GleanMetrics.h"
#include "mozilla/ipc/BackgroundUtils.h"
#include "mozilla/LoadInfo.h"
#include "mozilla/Logging.h"
#include "mozilla/net/EarlyHintRegistrar.h"
#include "mozilla/net/NeckoChannelParams.h"
@ -22,12 +25,14 @@
#include "nsAttrValue.h"
#include "nsAttrValueInlines.h"
#include "nsCOMPtr.h"
#include "nsContentPolicyUtils.h"
#include "nsContentSecurityManager.h"
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsHttpChannel.h"
#include "nsIAsyncVerifyRedirectCallback.h"
#include "nsIChannel.h"
#include "nsIContentSecurityPolicy.h"
#include "nsIHttpChannel.h"
#include "nsIInputStream.h"
#include "nsILoadInfo.h"
@ -188,7 +193,7 @@ void EarlyHintPreloader::MaybeCreateAndInsertPreload(
OngoingEarlyHints* aOngoingEarlyHints, const LinkHeader& aLinkHeader,
nsIURI* aBaseURI, nsIPrincipal* aPrincipal,
nsICookieJarSettings* aCookieJarSettings,
const nsACString& aResponseReferrerPolicy) {
const nsACString& aResponseReferrerPolicy, const nsACString& aCSPHeader) {
nsAttrValue as;
ParseAsValue(aLinkHeader.mAs, as);
@ -270,6 +275,73 @@ void EarlyHintPreloader::MaybeCreateAndInsertPreload(
corsMode, static_cast<ASDestination>(as.GetEnumValue()),
aLinkHeader.mType.LowerCaseEqualsASCII("module"));
// Verify that the resource should be loaded.
// This isn't the ideal way to test the resource against the CSP.
// The problem comes from the fact that at the stage of Early Hint
// processing we have not yet created a document where we would normally store
// the CSP.
// First we will create a load info,
// nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK
nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new LoadInfo(
aPrincipal, // loading principal
aPrincipal, // triggering principal
nullptr /* aLoadingContext node */,
nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, contentPolicyType);
if (aCSPHeader.Length() != 0) {
// If the CSP header is present then create a new CSP and apply the header
// directives to it
nsCOMPtr<nsIContentSecurityPolicy> csp = new nsCSPContext();
nsresult rv = csp->SetRequestContextWithPrincipal(
aPrincipal, aBaseURI, u""_ns, 0 /* aInnerWindowId */);
NS_ENSURE_SUCCESS_VOID(rv);
rv = CSP_AppendCSPFromHeader(csp, NS_ConvertUTF8toUTF16(aCSPHeader),
false /* report only */);
NS_ENSURE_SUCCESS_VOID(rv);
// We create a temporary ClientInfo. This is required on the loadInfo as
// that is how the CSP is queried. More specificially, as a hack to be able
// to call NS_CheckContentLoadPolicy on nsILoadInfo which exclusively
// accesses the CSP from the ClientInfo, we create a synthetic ClientInfo to
// hold the CSP we are creating. This is not a safe thing to do in any other
// circumstance because ClientInfos are always describing a ClientSource
// that corresponds to a global or potential global, so creating an info
// without a source is unsound. For the purposes of doing things before a
// global exists, fetch has the concept of a
// https://fetch.spec.whatwg.org/#concept-request-reserved-client and
// nsILoadInfo explicity has methods around GiveReservedClientSource which
// are primarily used by ClientChannelHelper. If you are trying to do real
// CSP stuff and the ClientInfo is not there yet, please enhance the logic
// around ClientChannelHelper.
mozilla::ipc::PrincipalInfo principalInfo;
rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo);
NS_ENSURE_SUCCESS_VOID(rv);
dom::ClientInfo clientInfo(nsID::GenerateUUID(), dom::ClientType::Window,
principalInfo, TimeStamp::Now());
// Our newly-created CSP is set on the ClientInfo via the indirect route of
// first serializing to CSPInfo
ipc::CSPInfo cspInfo;
rv = CSPToCSPInfo(csp, &cspInfo);
NS_ENSURE_SUCCESS_VOID(rv);
clientInfo.SetCspInfo(cspInfo);
// This ClientInfo is then set on the new loadInfo.
// It can now be used to test the resource against the policy
secCheckLoadInfo->SetClientInfo(clientInfo);
}
int16_t shouldLoad = nsIContentPolicy::ACCEPT;
nsresult rv =
NS_CheckContentLoadPolicy(uri, secCheckLoadInfo, ""_ns, &shouldLoad,
nsContentUtils::GetContentPolicy());
if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) {
return;
}
NS_ENSURE_SUCCESS_VOID(earlyHintPreloader->OpenChannel(
uri, aPrincipal, securityFlags, contentPolicyType, referrerInfo,
aCookieJarSettings));
@ -291,6 +363,7 @@ nsresult EarlyHintPreloader::OpenChannel(
aContentPolicyType == nsContentPolicyType::TYPE_SCRIPT ||
aContentPolicyType == nsContentPolicyType::TYPE_STYLESHEET ||
aContentPolicyType == nsContentPolicyType::TYPE_FONT);
nsresult rv =
NS_NewChannel(getter_AddRefs(mChannel), aURI, aPrincipal, aSecurityFlags,
aContentPolicyType, aCookieJarSettings,

View File

@ -81,7 +81,7 @@ class EarlyHintPreloader final : public nsIStreamListener,
OngoingEarlyHints* aOngoingEarlyHints, const LinkHeader& aHeader,
nsIURI* aBaseURI, nsIPrincipal* aPrincipal,
nsICookieJarSettings* aCookieJarSettings,
const nsACString& aReferrerPolicy);
const nsACString& aReferrerPolicy, const nsACString& aCSPHeader);
// register Channel to EarlyHintRegistrar returns connect arguments
EarlyHintConnectArgs Register();

View File

@ -31,7 +31,8 @@ EarlyHintsService::~EarlyHintsService() = default;
void EarlyHintsService::EarlyHint(const nsACString& aLinkHeader,
nsIURI* aBaseURI, nsIChannel* aChannel,
const nsACString& aReferrerPolicy) {
const nsACString& aReferrerPolicy,
const nsACString& aCSPHeader) {
mEarlyHintsCount++;
if (mFirstEarlyHint.isNothing()) {
mFirstEarlyHint.emplace(TimeStamp::NowLoRes());
@ -91,7 +92,7 @@ void EarlyHintsService::EarlyHint(const nsACString& aLinkHeader,
} else if (linkHeader.mRel.LowerCaseEqualsLiteral("preload")) {
EarlyHintPreloader::MaybeCreateAndInsertPreload(
mOngoingEarlyHints, linkHeader, aBaseURI, principal,
cookieJarSettings, aReferrerPolicy);
cookieJarSettings, aReferrerPolicy, aCSPHeader);
}
}
}

View File

@ -27,7 +27,8 @@ class EarlyHintsService {
EarlyHintsService();
~EarlyHintsService();
void EarlyHint(const nsACString& aLinkHeader, nsIURI* aBaseURI,
nsIChannel* aChannel, const nsACString& aReferrerPolicy);
nsIChannel* aChannel, const nsACString& aReferrerPolicy,
const nsACString& aCSPHeader);
void FinalResponse(uint32_t aResponseStatus);
void Cancel(const nsACString& aReason);

View File

@ -629,11 +629,12 @@ HttpTransactionChild::CheckListenerChain() {
}
NS_IMETHODIMP
HttpTransactionChild::EarlyHint(const nsACString& value,
const nsACString& referrerPolicy) {
HttpTransactionChild::EarlyHint(const nsACString& aValue,
const nsACString& aReferrerPolicy,
const nsACString& aCSPHeader) {
LOG(("HttpTransactionChild::EarlyHint"));
if (CanSend()) {
Unused << SendEarlyHint(value, referrerPolicy);
Unused << SendEarlyHint(aValue, aReferrerPolicy, aCSPHeader);
}
return NS_OK;
}

View File

@ -668,13 +668,17 @@ mozilla::ipc::IPCResult HttpTransactionParent::RecvOnH2PushStream(
} // namespace net
mozilla::ipc::IPCResult HttpTransactionParent::RecvEarlyHint(
const nsCString& aValue, const nsACString& aReferrerPolicy) {
LOG(("HttpTransactionParent::RecvEarlyHint header=%s aReferrerPolicy=%s",
const nsCString& aValue, const nsACString& aReferrerPolicy,
const nsACString& aCSPHeader) {
LOG(
("HttpTransactionParent::RecvEarlyHint header=%s aReferrerPolicy=%s "
"aCSPHeader=%s",
PromiseFlatCString(aValue).get(),
PromiseFlatCString(aReferrerPolicy).get()));
PromiseFlatCString(aReferrerPolicy).get(),
PromiseFlatCString(aCSPHeader).get()));
nsCOMPtr<nsIEarlyHintObserver> obs = do_QueryInterface(mChannel);
if (obs) {
Unused << obs->EarlyHint(aValue, aReferrerPolicy);
Unused << obs->EarlyHint(aValue, aReferrerPolicy, aCSPHeader);
}
return IPC_OK();

View File

@ -75,7 +75,8 @@ class HttpTransactionParent final : public PHttpTransactionParent,
const nsCString& aResourceUrl,
const nsCString& aRequestString);
mozilla::ipc::IPCResult RecvEarlyHint(const nsCString& aValue,
const nsACString& aReferrerPolicy);
const nsACString& aReferrerPolicy,
const nsACString& aCSPHeader);
virtual mozilla::TimeStamp GetPendingTime() override;

View File

@ -73,7 +73,7 @@ parent:
async OnH2PushStream(uint32_t pushedStreamId,
nsCString resourceUrl,
nsCString requestString);
async EarlyHint(nsCString linkHeader, nsCString referrerPolicy);
async EarlyHint(nsCString linkHeader, nsCString referrerPolicy, nsCString cspHeader);
child:
async __delete__();

View File

@ -38,6 +38,7 @@ HTTP_ATOM(Content_Length, "Content-Length")
HTTP_ATOM(Content_Location, "Content-Location")
HTTP_ATOM(Content_MD5, "Content-MD5")
HTTP_ATOM(Content_Range, "Content-Range")
HTTP_ATOM(Content_Security_Policy, "Content-Security-Policy")
HTTP_ATOM(Content_Type, "Content-Type")
HTTP_ATOM(Cookie, "Cookie")
HTTP_ATOM(Cross_Origin_Embedder_Policy, "Cross-Origin-Embedder-Policy")

View File

@ -9865,13 +9865,14 @@ nsHttpChannel::SetEarlyHintObserver(nsIEarlyHintObserver* aObserver) {
}
NS_IMETHODIMP
nsHttpChannel::EarlyHint(const nsACString& linkHeader,
const nsACString& referrerPolicy) {
nsHttpChannel::EarlyHint(const nsACString& aLinkHeader,
const nsACString& aReferrerPolicy,
const nsACString& aCspHeader) {
LOG(("nsHttpChannel::EarlyHint.\n"));
if (mEarlyHintObserver && nsContentUtils::ComputeIsSecureContext(this)) {
LOG(("nsHttpChannel::EarlyHint propagated.\n"));
mEarlyHintObserver->EarlyHint(linkHeader, referrerPolicy);
mEarlyHintObserver->EarlyHint(aLinkHeader, aReferrerPolicy, aCspHeader);
}
return NS_OK;
}

View File

@ -1974,6 +1974,10 @@ nsresult nsHttpTransaction::ParseLineSegment(char* segment, uint32_t len) {
referrerPolicy);
if (NS_SUCCEEDED(rv) && !linkHeader.IsEmpty()) {
nsCString cspHeader;
Unused << mResponseHead->GetHeader(nsHttp::Content_Security_Policy,
cspHeader);
nsCOMPtr<nsIEarlyHintObserver> earlyHint;
{
MutexAutoLock lock(mLock);
@ -1984,8 +1988,9 @@ nsresult nsHttpTransaction::ParseLineSegment(char* segment, uint32_t len) {
NS_NewRunnableFunction(
"nsIEarlyHintObserver->EarlyHint",
[obs{std::move(earlyHint)}, header{std::move(linkHeader)},
referrerPolicy{std::move(referrerPolicy)}]() {
obs->EarlyHint(header, referrerPolicy);
referrerPolicy{std::move(referrerPolicy)},
cspHeader{std::move(cspHeader)}]() {
obs->EarlyHint(header, referrerPolicy, cspHeader);
}),
NS_DISPATCH_NORMAL);
MOZ_ASSERT(NS_SUCCEEDED(rv));

View File

@ -5,8 +5,6 @@
#include "nsISupports.idl"
native ReferrerPolicy(mozilla::dom::ReferrerPolicy);
[scriptable, uuid(97b6be6f-283c-45dd-81a7-4bb2d87d42f8)]
interface nsIEarlyHintObserver : nsISupports
{
@ -14,5 +12,5 @@ interface nsIEarlyHintObserver : nsISupports
* This method is called when the transaction has early hint (i.e. the
* '103 Early Hint' informational response) headers.
*/
void earlyHint(in ACString linkHeader, in ACString referrerPolicy);
void earlyHint(in ACString linkHeader, in ACString referrerPolicy, in ACString cspHeader);
};

View File

@ -11,6 +11,7 @@ support-files =
early_hint_error.sjs
early_hint_asset.sjs
early_hint_asset_html.sjs
early_hint_csp_options_html.sjs
early_hint_preconnect_html.sjs
post.html
res.css
@ -93,10 +94,6 @@ support-files =
file_lnk.lnk
[browser_post_auth.js]
skip-if = socketprocess_networking # Bug 1772209
[browser_103_csp.js]
support-files =
early_hint_preload_test_helper.jsm
skip-if = true # Bug 1765289 superseds this test
[browser_backgroundtask_purgeHTTPCache.js]
skip-if =
os == "android" # MultiInstanceLock doesn't work on Android
@ -133,4 +130,13 @@ support-files =
support-files =
early_hint_referrer_policy_html.sjs
early_hint_preload_test_helper.jsm
[browser_103_csp.js]
support-files =
early_hint_preload_test_helper.jsm
[browser_103_csp_images.js]
support-files =
early_hint_preload_test_helper.jsm
[browser_103_csp_styles.js]
support-files =
early_hint_preload_test_helper.jsm
[browser_103_preconnect.js]

View File

@ -4,53 +4,83 @@
"use strict";
const { request_count_checking } = ChromeUtils.import(
Services.prefs.setBoolPref("network.early-hints.enabled", true);
const { test_preload_hint_and_request } = ChromeUtils.import(
"resource://testing-common/early_hint_preload_test_helper.jsm"
);
// csp header with "img-src: 'none'" only on main html response, don't show the image on the page
add_task(async function test_preload_csp_imgsrc_none() {
// reset the count
let headers = new Headers();
headers.append("X-Early-Hint-Count-Start", "");
await fetch(
"https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs",
{ headers }
);
let requestUrl =
"https://example.com/browser/netwerk/test/browser/103_preload_csp_imgsrc_none.html";
await BrowserTestUtils.withNewTab(
add_task(async function test_preload_images_csp_in_early_hints_response() {
let tests = [
{
gBrowser,
url: requestUrl,
waitForLoad: true,
input: {
test_name: "image - no csp",
resource_type: "image",
csp: "",
csp_in_early_hint: "",
host: "",
hinted: true,
},
expected: { hinted: 1, normal: 0 },
},
async function(browser) {
let noImgLoaded = await SpecialPowers.spawn(browser, [], function() {
let loadInfo = content.performance.getEntriesByName(
"https://example.com/browser/netwerk/test/browser/early_hint_pixel.sjs?1ac2a5e1-90c7-4171-b0f0-676f7d899af3"
);
return loadInfo.every(entry => entry.decodedBodySize === 0);
});
await Assert.ok(
noImgLoaded,
"test_preload_csp_imgsrc_none: Image dislpayed unexpectedly"
);
}
);
{
input: {
test_name: "image img-src 'self';",
resource_type: "image",
csp: "",
csp_in_early_hint: "img-src 'self';",
host: "",
hinted: true,
},
expected: { hinted: 1, normal: 0 },
},
{
input: {
test_name: "image img-src 'self'; same host provided",
resource_type: "image",
csp: "",
csp_in_early_hint: "img-src 'self';",
host: "https://example.com/browser/netwerk/test/browser/",
hinted: true,
},
expected: { hinted: 1, normal: 0 },
},
{
input: {
test_name: "image img-src 'self'; other host provided",
resource_type: "image",
csp: "",
csp_in_early_hint: "img-src 'self';",
host: "https://example.org/browser/netwerk/test/browser/",
hinted: true,
},
expected: { hinted: 0, normal: 1 },
},
{
input: {
test_name: "image img-src 'none';",
resource_type: "image",
csp: "",
csp_in_early_hint: "img-src 'none';",
host: "",
hinted: true,
},
expected: { hinted: 0, normal: 1 },
},
{
input: {
test_name: "image img-src 'none'; same host provided",
resource_type: "image",
csp: "",
csp_in_early_hint: "img-src 'none';",
host: "https://example.com/browser/netwerk/test/browser/",
hinted: true,
},
expected: { hinted: 0, normal: 1 },
},
];
let gotRequestCount = await fetch(
"https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs"
).then(response => response.json());
let expectedRequestCount = { hinted: 1, normal: 0 };
await request_count_checking(
"test_preload_csp_imgsrc_none",
gotRequestCount,
expectedRequestCount
);
Services.cache2.clear();
for (let test of tests) {
await test_preload_hint_and_request(test.input, test.expected);
}
});

View File

@ -0,0 +1,170 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
Services.prefs.setBoolPref("network.early-hints.enabled", true);
// This verifies hints, requests server-side and client-side that the image actually loaded
async function test_image_preload_hint_request_loaded(
input,
expected_results,
image_should_load
) {
// reset the count
let headers = new Headers();
headers.append("X-Early-Hint-Count-Start", "");
await fetch(
"https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs",
{ headers }
);
let requestUrl = `https://example.com/browser/netwerk/test/browser/early_hint_csp_options_html.sjs?as=${
input.resource_type
}&hinted=${input.hinted ? "1" : "0"}${input.csp ? "&csp=" + input.csp : ""}${
input.csp_in_early_hint
? "&csp_in_early_hint=" + input.csp_in_early_hint
: ""
}${input.host ? "&host=" + input.host : ""}`;
console.log("requestUrl: " + requestUrl);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: requestUrl,
waitForLoad: true,
},
async function(browser) {
let imageLoaded = await ContentTask.spawn(browser, [], function() {
let image = content.document.getElementById("test_image");
return image && image.complete && image.naturalHeight !== 0;
});
await Assert.ok(
image_should_load == imageLoaded,
"test_image_preload_hint_request_loaded: the image can be loaded as expected " +
requestUrl
);
}
);
let gotRequestCount = await fetch(
"https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs"
).then(response => response.json());
await Assert.deepEqual(gotRequestCount, expected_results, input.test_name);
Services.cache2.clear();
}
// These tests verify whether or not the image actually loaded in the document
add_task(async function test_images_loaded_with_csp() {
let tests = [
{
input: {
test_name: "image loaded - no csp",
resource_type: "image",
csp: "",
csp_in_early_hint: "",
host: "",
hinted: true,
},
expected: { hinted: 1, normal: 0 },
image_should_load: true,
},
{
input: {
test_name: "image loaded - img-src none",
resource_type: "image",
csp: "img-src 'none';",
csp_in_early_hint: "",
host: "",
hinted: true,
},
expected: { hinted: 1, normal: 0 },
image_should_load: false,
},
{
input: {
test_name: "image loaded - img-src none in EH response",
resource_type: "image",
csp: "",
csp_in_early_hint: "img-src 'none';",
host: "",
hinted: true,
},
expected: { hinted: 0, normal: 1 },
image_should_load: true,
},
{
input: {
test_name: "image loaded - img-src none in both headers",
resource_type: "image",
csp: "img-src 'none';",
csp_in_early_hint: "img-src 'none';",
host: "",
hinted: true,
},
expected: { hinted: 0, normal: 0 },
image_should_load: false,
},
{
input: {
test_name: "image loaded - img-src self",
resource_type: "image",
csp: "img-src 'self';",
csp_in_early_hint: "",
host: "",
hinted: true,
},
expected: { hinted: 1, normal: 0 },
image_should_load: true,
},
{
input: {
test_name: "image loaded - img-src self in EH response",
resource_type: "image",
csp: "",
csp_in_early_hint: "img-src 'self';",
host: "",
hinted: true,
},
expected: { hinted: 1, normal: 0 },
image_should_load: true,
},
{
input: {
test_name: "image loaded - conflicting csp, early hint skipped",
resource_type: "image",
csp: "img-src 'self';",
csp_in_early_hint: "img-src 'none';",
host: "",
hinted: true,
},
expected: { hinted: 0, normal: 1 },
image_should_load: true,
},
{
input: {
test_name:
"image loaded - conflicting csp, resource not loaded in document",
resource_type: "image",
csp: "img-src 'none';",
csp_in_early_hint: "img-src 'self';",
host: "",
hinted: true,
},
expected: { hinted: 1, normal: 0 },
image_should_load: false,
},
];
for (let test of tests) {
await test_image_preload_hint_request_loaded(
test.input,
test.expected,
test.image_should_load
);
}
});

View File

@ -0,0 +1,86 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
Services.prefs.setBoolPref("network.early-hints.enabled", true);
const { test_preload_hint_and_request } = ChromeUtils.import(
"resource://testing-common/early_hint_preload_test_helper.jsm"
);
add_task(async function test_preload_styles_csp_in_response() {
let tests = [
{
input: {
test_name: "style - no csp",
resource_type: "style",
csp: "",
csp_in_early_hint: "",
host: "",
hinted: true,
},
expected: { hinted: 1, normal: 0 },
},
{
input: {
test_name: "style style-src 'self';",
resource_type: "style",
csp: "",
csp_in_early_hint: "style-src 'self';",
host: "",
hinted: true,
},
expected: { hinted: 1, normal: 0 },
},
{
input: {
test_name: "style style-src self; same host provided",
resource_type: "style",
csp: "",
csp_in_early_hint: "style-src 'self';",
host: "https://example.com/browser/netwerk/test/browser/",
hinted: true,
},
expected: { hinted: 1, normal: 0 },
},
{
input: {
test_name: "style style-src 'self'; other host provided",
resource_type: "style",
csp: "",
csp_in_early_hint: "style-src 'self';",
host: "https://example.org/browser/netwerk/test/browser/",
hinted: true,
},
expected: { hinted: 0, normal: 1 },
},
{
input: {
test_name: "style style-src 'none';",
resource_type: "style",
csp: "",
csp_in_early_hint: "style-src 'none';",
host: "",
hinted: true,
},
expected: { hinted: 0, normal: 1 },
},
{
input: {
test_name: "style style-src 'none'; other host provided",
resource_type: "style",
csp: "",
csp_in_early_hint: "style-src 'none';",
host: "https://example.org/browser/netwerk/test/browser/",
hinted: true,
},
expected: { hinted: 0, normal: 1 },
},
];
for (let test of tests) {
await test_preload_hint_and_request(test.input, test.expected);
}
});

View File

@ -0,0 +1,121 @@
"use strict";
function handleRequest(request, response) {
Cu.importGlobalProperties(["URLSearchParams"]);
let qs = new URLSearchParams(request.queryString);
let asset = qs.get("as");
let hinted = qs.get("hinted") !== "0";
let httpCode = qs.get("code");
let csp = qs.get("csp");
let csp_in_early_hint = qs.get("csp_in_early_hint");
let host = qs.get("host");
// eslint-disable-next-line mozilla/use-services
let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(
Ci.nsIUUIDGenerator
);
let uuid = uuidGenerator.generateUUID().toString();
let url = `early_hint_pixel.sjs?as=${asset}&uuid=${uuid}`;
if (host) {
url = host + url;
}
// write to raw socket
response.seizePower();
if (hinted) {
response.write("HTTP/1.1 103 Early Hint\r\n");
if (csp_in_early_hint) {
response.write(
`Content-Security-Policy: ${csp_in_early_hint.replaceAll('"', "")}\r\n`
);
}
response.write(`Link: <${url}>; rel=preload; as=${asset}\r\n`);
response.write("\r\n");
}
let body = "";
if (asset === "image") {
body = `<!DOCTYPE html>
<html>
<body>
<img id="test_image" src="${url}" width="100px">
</body>
</html>`;
} else if (asset === "style") {
body = `<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="${url}">
</head>
<body>
<h1>Test preload css<h1>
<div id="square" style="width:100px;height:100px;">
</body>
</html>
`;
} else if (asset === "script") {
body = `<!DOCTYPE html>
<html>
<head>
<script src="${url}"></script>
</head>
<body>
<h1>Test preload javascript<h1>
<div id="square" style="width:100px;height:100px;">
</body>
</html>
`;
} else if (asset === "fetch") {
body = `<!DOCTYPE html>
<html>
<body onload="onLoad()">
<script>
function onLoad() {
fetch("${url}")
.then(r => r.text())
.then(r => document.getElementsByTagName("h2")[0].textContent = r);
}
</script>
<h1>Test preload fetch</h1>
<h2>Fetching...</h2>
</body>
</html>
`;
} else if (asset === "font") {
body = `<!DOCTYPE html>
<html>
<head>
<style>
@font-face {
font-family: "preloadFont";
src: url("${url}") format("woff");
}
body {
font-family: "preloadFont";
}
</style>
</head>
<body>
<h1>Test preload font<h1>
</body>
</html>
`;
}
if (!httpCode) {
response.write(`HTTP/1.1 200 OK\r\n`);
} else {
response.write(`HTTP/1.1 ${httpCode} Error\r\n`);
}
response.write("Content-Type: text/html;charset=utf-8\r\n");
response.write("Cache-Control: no-cache\r\n");
response.write(`Content-Length: ${body.length}\r\n`);
if (csp) {
response.write(`Content-Security-Policy: ${csp.replaceAll('"', "")}\r\n`);
}
response.write("\r\n");
response.write(body);
response.finish();
}

View File

@ -8,6 +8,7 @@ const EXPORTED_SYMBOLS = [
"request_count_checking",
"test_hint_preload",
"test_hint_preload_internal",
"test_preload_hint_and_request",
];
const { Assert } = ChromeUtils.importESModule(
@ -96,3 +97,47 @@ async function test_hint_preload_internal(
await request_count_checking(testName, gotRequestCount, expectedRequestCount);
}
// Verify that CSP policies in both the 103 response as well as the main response are respected.
// e.g.
// 103 Early Hint
// Content-Security-Policy: style-src: self;
// Link: </style.css>; rel=preload; as=style
// 200 OK
// Content-Security-Policy: style-src: none;
// Link: </font.ttf>; rel=preload; as=font
// Server-side we verify that:
// - the hinted preload request was made as expected
// - the load request request was made as expected
// Client-side, we verify that the image was loaded or not loaded, depending on the scenario
// This verifies preload hints and requests
async function test_preload_hint_and_request(input, expected_results) {
// reset the count
let headers = new Headers();
headers.append("X-Early-Hint-Count-Start", "");
await fetch(
"https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs",
{ headers }
);
let requestUrl = `https://example.com/browser/netwerk/test/browser/early_hint_csp_options_html.sjs?as=${
input.resource_type
}&hinted=${input.hinted ? "1" : "0"}${input.csp ? "&csp=" + input.csp : ""}${
input.csp_in_early_hint
? "&csp_in_early_hint=" + input.csp_in_early_hint
: ""
}${input.host ? "&host=" + input.host : ""}`;
await BrowserTestUtils.openNewForegroundTab(gBrowser, requestUrl, true);
let gotRequestCount = await fetch(
"https://example.com/browser/netwerk/test/browser/early_hint_pixel_count.sjs"
).then(response => response.json());
await Assert.deepEqual(gotRequestCount, expected_results, input.test_name);
gBrowser.removeCurrentTab();
Services.cache2.clear();
}