blob: 09c1190710956fb7f32a60a44710523158b78661 [file] [log] [blame]
/*
* Copyright (C) 2021 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "CrossOriginOpenerPolicy.h"
#include "ContentSecurityPolicy.h"
#include "CrossOriginEmbedderPolicy.h"
#include "FormData.h"
#include "Frame.h"
#include "HTTPHeaderNames.h"
#include "HTTPParsers.h"
#include "NavigationRequester.h"
#include "Page.h"
#include "PingLoader.h"
#include "ResourceResponse.h"
#include "ScriptExecutionContext.h"
#include "SecurityPolicy.h"
#include <wtf/JSONValues.h>
namespace WebCore {
static ASCIILiteral crossOriginOpenerPolicyToString(const CrossOriginOpenerPolicyValue& coop)
{
switch (coop) {
case CrossOriginOpenerPolicyValue::SameOrigin:
case CrossOriginOpenerPolicyValue::SameOriginPlusCOEP:
return "same-origin"_s;
case CrossOriginOpenerPolicyValue::SameOriginAllowPopups:
return "same-origin-allow-popups"_s;
case CrossOriginOpenerPolicyValue::UnsafeNone:
break;
}
return "unsafe-none"_s;
}
static ASCIILiteral crossOriginOpenerPolicyValueToEffectivePolicyString(CrossOriginOpenerPolicyValue coop)
{
switch (coop) {
case CrossOriginOpenerPolicyValue::SameOriginAllowPopups:
return "same-origin-allow-popups"_s;
case CrossOriginOpenerPolicyValue::SameOrigin:
return "same-origin"_s;
case CrossOriginOpenerPolicyValue::SameOriginPlusCOEP:
return "same-origin-plus-coep"_s;
case CrossOriginOpenerPolicyValue::UnsafeNone:
break;
}
return "unsafe-none"_s;
}
// https://html.spec.whatwg.org/multipage/origin.html#check-browsing-context-group-switch-coop-value
static bool checkIfCOOPValuesRequireBrowsingContextGroupSwitch(bool isInitialAboutBlank, CrossOriginOpenerPolicyValue activeDocumentCOOPValue, const SecurityOrigin& activeDocumentNavigationOrigin, CrossOriginOpenerPolicyValue responseCOOPValue, const SecurityOrigin& responseOrigin)
{
// If the result of matching activeDocumentCOOPValue, activeDocumentNavigationOrigin, responseCOOPValue, and responseOrigin is true, return false.
// https://html.spec.whatwg.org/multipage/origin.html#matching-coop
if (activeDocumentCOOPValue == CrossOriginOpenerPolicyValue::UnsafeNone && responseCOOPValue == CrossOriginOpenerPolicyValue::UnsafeNone)
return false;
if (activeDocumentCOOPValue == responseCOOPValue && activeDocumentNavigationOrigin.isSameOriginAs(responseOrigin))
return false;
// If all of the following are true:
// - isInitialAboutBlank,
// - activeDocumentCOOPValue's value is "same-origin-allow-popups".
// - responseCOOPValue is "unsafe-none",
// then return false.
if (isInitialAboutBlank && activeDocumentCOOPValue == CrossOriginOpenerPolicyValue::SameOriginAllowPopups && responseCOOPValue == CrossOriginOpenerPolicyValue::UnsafeNone)
return false;
return true;
}
// https://html.spec.whatwg.org/multipage/origin.html#check-bcg-switch-navigation-report-only
static bool checkIfEnforcingReportOnlyCOOPWouldRequireBrowsingContextGroupSwitch(bool isInitialAboutBlank, const CrossOriginOpenerPolicy& activeDocumentCOOP, const SecurityOrigin& activeDocumentNavigationOrigin, const CrossOriginOpenerPolicy& responseCOOP, const SecurityOrigin& responseOrigin)
{
if (!checkIfCOOPValuesRequireBrowsingContextGroupSwitch(isInitialAboutBlank, activeDocumentCOOP.reportOnlyValue, activeDocumentNavigationOrigin, responseCOOP.reportOnlyValue, responseOrigin))
return false;
if (checkIfCOOPValuesRequireBrowsingContextGroupSwitch(isInitialAboutBlank, activeDocumentCOOP.reportOnlyValue, activeDocumentNavigationOrigin, responseCOOP.value, responseOrigin))
return true;
if (checkIfCOOPValuesRequireBrowsingContextGroupSwitch(isInitialAboutBlank, activeDocumentCOOP.value, activeDocumentNavigationOrigin, responseCOOP.reportOnlyValue, responseOrigin))
return true;
return false;
}
static std::tuple<Ref<SecurityOrigin>, CrossOriginOpenerPolicy> computeResponseOriginAndCOOP(const ResourceResponse& response, const std::optional<NavigationRequester>& requester, ContentSecurityPolicy* responseCSP)
{
// Non-initial empty documents (about:blank) should inherit their cross-origin-opener-policy from the navigation's initiator top level document,
// if the initiator and its top level document are same-origin, or default (unsafe-none) otherwise.
// https://github.com/whatwg/html/issues/6913
if (SecurityPolicy::shouldInheritSecurityOriginFromOwner(response.url()) && requester)
return std::make_tuple(requester->securityOrigin, requester->securityOrigin->isSameOriginAs(requester->topOrigin) ? requester->crossOriginOpenerPolicy : CrossOriginOpenerPolicy { });
// If the HTTP response contains a CSP header, it may set sandbox flags, which would cause the origin to become unique.
auto responseOrigin = responseCSP && responseCSP->sandboxFlags() != SandboxNone ? SecurityOrigin::createUnique() : SecurityOrigin::create(response.url());
return std::make_tuple(WTFMove(responseOrigin), obtainCrossOriginOpenerPolicy(response));
}
// https://html.spec.whatwg.org/multipage/origin.html#coop-enforce
static CrossOriginOpenerPolicyEnforcementResult enforceResponseCrossOriginOpenerPolicy(const CrossOriginOpenerPolicyEnforcementResult& currentCoopEnforcementResult, const URL& responseURL, SecurityOrigin& responseOrigin, const CrossOriginOpenerPolicy& responseCOOP, bool isDisplayingInitialEmptyDocument, const Function<void(COOPDisposition)>& sendViolationReports)
{
CrossOriginOpenerPolicyEnforcementResult newCOOPEnforcementResult = {
responseURL,
responseOrigin,
responseCOOP,
true /* isCurrentContextNavigationSource */,
currentCoopEnforcementResult.needsBrowsingContextGroupSwitch,
currentCoopEnforcementResult.needsBrowsingContextGroupSwitchDueToReportOnly
};
if (checkIfCOOPValuesRequireBrowsingContextGroupSwitch(isDisplayingInitialEmptyDocument, currentCoopEnforcementResult.crossOriginOpenerPolicy.value, currentCoopEnforcementResult.currentOrigin, responseCOOP.value, responseOrigin)) {
newCOOPEnforcementResult.needsBrowsingContextGroupSwitch = true;
sendViolationReports(COOPDisposition::Enforce);
}
if (checkIfEnforcingReportOnlyCOOPWouldRequireBrowsingContextGroupSwitch(isDisplayingInitialEmptyDocument, currentCoopEnforcementResult.crossOriginOpenerPolicy, currentCoopEnforcementResult.currentOrigin, responseCOOP, responseOrigin)) {
newCOOPEnforcementResult.needsBrowsingContextGroupSwitchDueToReportOnly = true;
sendViolationReports(COOPDisposition::Reporting);
}
return newCOOPEnforcementResult;
}
// https://html.spec.whatwg.org/multipage/origin.html#obtain-coop
CrossOriginOpenerPolicy obtainCrossOriginOpenerPolicy(const ResourceResponse& response)
{
std::optional<CrossOriginEmbedderPolicy> coep;
auto ensureCOEP = [&coep, &response]() -> CrossOriginEmbedderPolicy& {
if (!coep)
coep = obtainCrossOriginEmbedderPolicy(response, nullptr);
return *coep;
};
auto parseCOOP = [&response, &ensureCOEP](HTTPHeaderName headerName, auto& value, auto& reportingEndpoint) {
auto coopParsingResult = parseStructuredFieldValue(response.httpHeaderField(headerName));
if (!coopParsingResult)
return;
if (coopParsingResult->first == "same-origin") {
auto& coep = ensureCOEP();
if (coep.value == CrossOriginEmbedderPolicyValue::RequireCORP || (headerName == HTTPHeaderName::CrossOriginOpenerPolicyReportOnly && coep.reportOnlyValue == CrossOriginEmbedderPolicyValue::RequireCORP))
value = CrossOriginOpenerPolicyValue::SameOriginPlusCOEP;
else
value = CrossOriginOpenerPolicyValue::SameOrigin;
} else if (coopParsingResult->first == "same-origin-allow-popups")
value = CrossOriginOpenerPolicyValue::SameOriginAllowPopups;
reportingEndpoint = coopParsingResult->second.get("report-to"_s);
};
CrossOriginOpenerPolicy policy;
if (!SecurityOrigin::create(response.url())->isPotentiallyTrustworthy())
return policy;
parseCOOP(HTTPHeaderName::CrossOriginOpenerPolicy, policy.value, policy.reportingEndpoint);
parseCOOP(HTTPHeaderName::CrossOriginOpenerPolicyReportOnly, policy.reportOnlyValue, policy.reportOnlyReportingEndpoint);
return policy;
}
CrossOriginOpenerPolicy CrossOriginOpenerPolicy::isolatedCopy() const
{
return {
value,
reportingEndpoint.isolatedCopy(),
reportOnlyValue,
reportOnlyReportingEndpoint.isolatedCopy()
};
}
void addCrossOriginOpenerPolicyHeaders(ResourceResponse& response, const CrossOriginOpenerPolicy& coop)
{
if (coop.value != CrossOriginOpenerPolicyValue::UnsafeNone) {
if (coop.reportingEndpoint.isEmpty())
response.setHTTPHeaderField(HTTPHeaderName::CrossOriginOpenerPolicy, crossOriginOpenerPolicyToString(coop.value));
else
response.setHTTPHeaderField(HTTPHeaderName::CrossOriginOpenerPolicy, makeString(crossOriginOpenerPolicyToString(coop.value), "; report-to=\"", coop.reportingEndpoint, '\"'));
}
if (coop.reportOnlyValue != CrossOriginOpenerPolicyValue::UnsafeNone) {
if (coop.reportOnlyReportingEndpoint.isEmpty())
response.setHTTPHeaderField(HTTPHeaderName::CrossOriginOpenerPolicyReportOnly, crossOriginOpenerPolicyToString(coop.reportOnlyValue));
else
response.setHTTPHeaderField(HTTPHeaderName::CrossOriginOpenerPolicyReportOnly, makeString(crossOriginOpenerPolicyToString(coop.reportOnlyValue), "; report-to=\"", coop.reportOnlyReportingEndpoint, '\"'));
}
}
// https://html.spec.whatwg.org/multipage/origin.html#coop-violation-navigation-to
void sendViolationReportWhenNavigatingToCOOPResponse(Frame& frame, CrossOriginOpenerPolicy coop, COOPDisposition disposition, const URL& coopURL, const URL& previousResponseURL, const SecurityOrigin& coopOrigin, const SecurityOrigin& previousResponseOrigin, const String& referrer, const String& userAgent)
{
if (!frame.settings().coopCoepViolationReportingEnabled())
return;
auto& endpoint = coop.reportingEndpointForDisposition(disposition);
if (endpoint.isEmpty())
return;
PingLoader::sendReportToEndpoint(frame, coopOrigin.data(), endpoint, "coop"_s, coopURL, userAgent, [&](auto& body) {
body.setString("disposition"_s, disposition == COOPDisposition::Reporting ? "reporting"_s : "enforce"_s);
body.setString("effectivePolicy"_s, crossOriginOpenerPolicyValueToEffectivePolicyString(disposition == COOPDisposition::Reporting ? coop.reportOnlyValue : coop.value));
body.setString("previousResponseURL"_s, coopOrigin.isSameOriginAs(previousResponseOrigin) ? PingLoader::sanitizeURLForReport(previousResponseURL) : String());
body.setString("type"_s, "navigation-to-response"_s);
body.setString("referrer"_s, referrer);
});
}
// https://html.spec.whatwg.org/multipage/origin.html#coop-violation-navigation-from
void sendViolationReportWhenNavigatingAwayFromCOOPResponse(Frame& frame, CrossOriginOpenerPolicy coop, COOPDisposition disposition, const URL& coopURL, const URL& nextResponseURL, const SecurityOrigin& coopOrigin, const SecurityOrigin& nextResponseOrigin, bool isCOOPResponseNavigationSource, const String& userAgent)
{
if (!frame.settings().coopCoepViolationReportingEnabled())
return;
auto& endpoint = coop.reportingEndpointForDisposition(disposition);
if (endpoint.isEmpty())
return;
PingLoader::sendReportToEndpoint(frame, coopOrigin.data(), endpoint, "coop"_s, coopURL, userAgent, [&](auto& body) {
body.setString("disposition"_s, disposition == COOPDisposition::Reporting ? "reporting"_s : "enforce"_s);
body.setString("effectivePolicy"_s, crossOriginOpenerPolicyValueToEffectivePolicyString(disposition == COOPDisposition::Reporting ? coop.reportOnlyValue : coop.value));
body.setString("nextResponseURL"_s, coopOrigin.isSameOriginAs(nextResponseOrigin) || isCOOPResponseNavigationSource ? PingLoader::sanitizeURLForReport(nextResponseURL) : String());
body.setString("type"_s, "navigation-from-response"_s);
});
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#process-a-navigate-fetch (Step 13.5.6)
std::optional<CrossOriginOpenerPolicyEnforcementResult> doCrossOriginOpenerHandlingOfResponse(const ResourceResponse& response, const std::optional<NavigationRequester>& requester, ContentSecurityPolicy* responseCSP, SandboxFlags effectiveSandboxFlags, bool isDisplayingInitialEmptyDocument, const CrossOriginOpenerPolicyEnforcementResult& currentCoopEnforcementResult, const Function<void(COOPDisposition disposition, const CrossOriginOpenerPolicy& responseCOOP, const SecurityOrigin& responseOrigin)>& sendViolationReports)
{
auto [responseOrigin, responseCOOP] = computeResponseOriginAndCOOP(response, requester, responseCSP);
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#process-a-navigate-fetch (Step 13.5.6.2)
// If sandboxFlags is not empty and responseCOOP's value is not "unsafe-none", then set response to an appropriate network error and break.
if (responseCOOP.value != CrossOriginOpenerPolicyValue::UnsafeNone && effectiveSandboxFlags != SandboxNone)
return std::nullopt;
return enforceResponseCrossOriginOpenerPolicy(currentCoopEnforcementResult, response.url(), responseOrigin, responseCOOP, isDisplayingInitialEmptyDocument, [&, responseOrigin = responseOrigin, responseCOOP = responseCOOP](COOPDisposition disposition) {
sendViolationReports(disposition, responseCOOP, responseOrigin);
});
}
CrossOriginOpenerPolicyEnforcementResult CrossOriginOpenerPolicyEnforcementResult::from(const URL& currentURL, Ref<SecurityOrigin>&& currentOrigin, const CrossOriginOpenerPolicy& crossOriginOpenerPolicy, std::optional<NavigationRequester> requester, const URL& openerURL)
{
CrossOriginOpenerPolicyEnforcementResult result { currentURL, WTFMove(currentOrigin), crossOriginOpenerPolicy };
result.isCurrentContextNavigationSource = requester && result.currentOrigin->isSameOriginAs(requester->securityOrigin);
if (SecurityPolicy::shouldInheritSecurityOriginFromOwner(currentURL) && openerURL.isValid())
result.url = openerURL;
return result;
}
} // namespace WebCore