Skip to content

Commit

Permalink
Check CSP and X-Frame-Options for subframe AppSSO
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=260100
rdar://108625087

Reviewed by Alex Christensen.

Before this patch, AppSSO unconditionally sets cookies whenever
a session occurs without considering the headers in the response.
This patch starts considering CSP and X-Frame-Options for AppSSO responses.

Added API tests for this behavior.

* Source/WebKit/UIProcess/API/APIFrameInfo.h:
* Source/WebKit/UIProcess/Cocoa/SOAuthorization/SOAuthorizationSession.h:
* Source/WebKit/UIProcess/Cocoa/SOAuthorization/SOAuthorizationSession.mm:
(WebKit::SOAuthorizationSession::complete):
(WebKit::SOAuthorizationSession::shouldInterruptLoadForXFrameOptions):
(WebKit::SOAuthorizationSession::shouldInterruptLoadForCSPFrameAncestorsOrXFrameOptions):
(): Deleted.
* Tools/TestWebKitAPI/Tests/WebKitCocoa/SOAuthorizationTests.mm:
(TestWebKitAPI::TEST):

Originally-landed-as: 265870.403@safari-7616-branch (43c01fe). rdar://117809541
Canonical link: https://commits.webkit.org/270141@main
  • Loading branch information
pascoej authored and robert-jenner committed Nov 2, 2023
1 parent 9507587 commit 70c1251
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class NavigationAction;

namespace WebCore {
class ResourceResponse;
class SecurityOrigin;
}

namespace WebKit {
Expand Down Expand Up @@ -113,6 +114,9 @@ class SOAuthorizationSession : public ThreadSafeRefCountedAndCanMakeThreadSafeWe
void continueStartAfterGetAuthorizationHints(const String&);
void continueStartAfterDecidePolicy(const SOAuthorizationLoadPolicy&);

bool shouldInterruptLoadForCSPFrameAncestorsOrXFrameOptions(const WebCore::ResourceResponse&);
bool shouldInterruptLoadForXFrameOptions(Vector<RefPtr<WebCore::SecurityOrigin>>&& frameAncestorOrigins, const String& xFrameOptions, const URL&);

State m_state { State::Idle };
RetainPtr<SOAuthorization> m_soAuthorization;
RefPtr<API::NavigationAction> m_navigationAction;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

#if HAVE(APP_SSO)

#import "APIFrameHandle.h"
#import "APIHTTPCookieStore.h"
#import "APINavigation.h"
#import "APINavigationAction.h"
Expand All @@ -40,6 +41,8 @@
#import "WebFrameProxy.h"
#import "WebPageProxy.h"
#import "WebsiteDataStore.h"
#import <WebCore/ContentSecurityPolicy.h>
#import <WebCore/HTTPParsers.h>
#import <WebCore/ResourceResponse.h>
#import <WebCore/SecurityOrigin.h>
#import <pal/cocoa/AppSSOSoftLink.h>
Expand All @@ -49,6 +52,7 @@
#define AUTHORIZATIONSESSION_RELEASE_LOG(fmt, ...) RELEASE_LOG(AppSSO, "%p - [InitiatingAction=%s][State=%s] SOAuthorizationSession::" fmt, this, toString(m_action), stateString(), ##__VA_ARGS__)

namespace WebKit {
using namespace WebCore;

namespace {

Expand Down Expand Up @@ -305,6 +309,13 @@ static bool isSameOrigin(const WebCore::ResourceRequest& request, const WebCore:
becomeCompleted();

auto response = WebCore::ResourceResponse(httpResponse);

if (shouldInterruptLoadForCSPFrameAncestorsOrXFrameOptions(response)) {
AUTHORIZATIONSESSION_RELEASE_LOG("complete: CSP failed. Falling back to web path.");
fallBackToWebPathInternal();
return;
}

if (!isSameOrigin(m_navigationAction->request(), response)) {
AUTHORIZATIONSESSION_RELEASE_LOG("complete: Origins don't match. Falling back to web path.");
fallBackToWebPathInternal();
Expand Down Expand Up @@ -393,6 +404,69 @@ static bool isSameOrigin(const WebCore::ResourceRequest& request, const WebCore:
uiCallback(YES, nil);
}

bool SOAuthorizationSession::shouldInterruptLoadForXFrameOptions(Vector<RefPtr<SecurityOrigin>>&& frameAncestorOrigins, const String& xFrameOptions, const URL& url)
{
switch (parseXFrameOptionsHeader(xFrameOptions)) {
case XFrameOptionsDisposition::None:
case XFrameOptionsDisposition::AllowAll:
return false;
case XFrameOptionsDisposition::Deny:
return true;
case XFrameOptionsDisposition::SameOrigin: {
auto origin = SecurityOrigin::create(url);
for (auto& ancestorOrigin : frameAncestorOrigins) {
if (!origin->isSameSchemeHostPort(*ancestorOrigin))
return true;
}
return false;
}
case XFrameOptionsDisposition::Conflict: {
String errorMessage = "Multiple 'X-Frame-Options' headers with conflicting values ('" + xFrameOptions + "') encountered. Falling back to 'DENY'.";
AUTHORIZATIONSESSION_RELEASE_LOG("shouldInterruptLoadForXFrameOptions: %s", errorMessage.utf8().data());
return true;
}
case XFrameOptionsDisposition::Invalid: {
String errorMessage = "Invalid 'X-Frame-Options' header encountered: '" + xFrameOptions + "' is not a recognized directive. The header will be ignored.";
AUTHORIZATIONSESSION_RELEASE_LOG("shouldInterruptLoadForXFrameOptions: %s", errorMessage.utf8().data());
return false;
}
}
ASSERT_NOT_REACHED();
return false;
}

bool SOAuthorizationSession::shouldInterruptLoadForCSPFrameAncestorsOrXFrameOptions(const WebCore::ResourceResponse& response)
{
Vector<RefPtr<SecurityOrigin>> frameAncestorOrigins;
if (auto* targetFrame = m_navigationAction->targetFrame()) {
if (auto parentFrameHandle = targetFrame->parentFrameHandle()) {
for (auto* parent = WebFrameProxy::webFrame(parentFrameHandle->frameID()); parent; parent = parent->parentFrame()) {
auto origin = SecurityOrigin::create(parent->url());
RefPtr<SecurityOrigin> frameOrigin = origin.ptr();
frameAncestorOrigins.append(frameOrigin);
}
}
}

auto url = response.url();
ContentSecurityPolicy contentSecurityPolicy { URL { url }, nullptr, nullptr };
contentSecurityPolicy.didReceiveHeaders(ContentSecurityPolicyResponseHeaders { response }, m_navigationAction->request().httpReferrer());
if (!contentSecurityPolicy.allowFrameAncestors(frameAncestorOrigins, url))
return true;

if (!contentSecurityPolicy.overridesXFrameOptions()) {
String xFrameOptions = response.httpHeaderField(HTTPHeaderName::XFrameOptions);
if (!xFrameOptions.isNull() && shouldInterruptLoadForXFrameOptions(WTFMove(frameAncestorOrigins), xFrameOptions, response.url())) {
String errorMessage = makeString("Refused to display '", response.url().stringCenterEllipsizedToLength(), "' in a frame because it set 'X-Frame-Options' to '", xFrameOptions, "'.");
AUTHORIZATIONSESSION_RELEASE_LOG("shouldInterruptLoadForCSPFrameAncestorsOrXFrameOptions: %s", errorMessage.utf8().data());

return true;
}
}

return false;
}

#if PLATFORM(MAC)
void SOAuthorizationSession::dismissModalSheetIfNecessary()
{
Expand Down
67 changes: 64 additions & 3 deletions Tools/TestWebKitAPI/Tests/WebKitCocoa/SOAuthorizationTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2744,11 +2744,72 @@ static void checkAuthorizationOptions(bool userActionInitiated, String initiator

checkAuthorizationOptions(false, "null"_s, 2);
EXPECT_TRUE(policyForAppSSOPerformed);

[messageHandler extendExpectations:@[@"http://www.example.com", @"Hello."]];
[messageHandler extendExpectations:@[@"http://www.example.com", @"Hello.", @"http://www.example.com", @"Cookies: sessionid=38afes7a8"]];

auto response = adoptNS([[NSHTTPURLResponse alloc] initWithURL:testURL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:@{ @"Set-Cookie" : @"sessionid=38afes7a8;"}]);
auto iframeHtmlCString = generateHtml(iframeTemplate, emptyString()).utf8();
auto iframeHtmlCString = generateHtml(iframeTemplate, "parent.postMessage('Cookies: ' + document.cookie, '*');"_s).utf8();
[gDelegate authorization:gAuthorization didCompleteWithHTTPResponse:response.get() httpBody:adoptNS([[NSData alloc] initWithBytes:iframeHtmlCString.data() length:iframeHtmlCString.length()]).get()];
Util::run(&allMessagesReceived);
}

TEST(SOAuthorizationSubFrame, InterceptionSucceedWithCookieButCSPDeny)
{
resetState();
SWIZZLE_SOAUTH(PAL::getSOAuthorizationClass());
SWIZZLE_AKAUTH();

URL testURL { "http://www.example.com"_str };
auto testHtml = generateHtml(parentTemplate, testURL.string());

auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
auto messageHandler = adoptNS([[TestSOAuthorizationScriptMessageHandler alloc] initWithExpectation:@[@"http://www.example.com", @"SOAuthorizationDidStart"]]);
[[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"testHandler"];

auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500) configuration:configuration.get()]);
auto delegate = adoptNS([[TestSOAuthorizationDelegate alloc] init]);
configureSOAuthorizationWebView(webView.get(), delegate.get());

[webView loadHTMLString:testHtml baseURL:nil];
Util::run(&allMessagesReceived);

checkAuthorizationOptions(false, "null"_s, 2);
EXPECT_TRUE(policyForAppSSOPerformed);

[messageHandler extendExpectations:@[@"http://www.example.com", @"SOAuthorizationDidCancel"]];

auto response = adoptNS([[NSHTTPURLResponse alloc] initWithURL:testURL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:@{ @"Set-Cookie" : @"sessionid=38afes7a8;", @"Content-Security-Policy" : @"frame-ancestors 'none';" }]);
auto iframeHtmlCString = generateHtml(iframeTemplate, "parent.postMessage('Cookies: ' + document.cookie, '*');"_s).utf8();
[gDelegate authorization:gAuthorization didCompleteWithHTTPResponse:response.get() httpBody:adoptNS([[NSData alloc] initWithBytes:iframeHtmlCString.data() length:iframeHtmlCString.length()]).get()];
Util::run(&allMessagesReceived);
}

TEST(SOAuthorizationSubFrame, InterceptionSucceedWithCookieButXFrameDeny)
{
resetState();
SWIZZLE_SOAUTH(PAL::getSOAuthorizationClass());
SWIZZLE_AKAUTH();

URL testURL { "http://www.example.com"_str };
auto testHtml = generateHtml(parentTemplate, testURL.string());

auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
auto messageHandler = adoptNS([[TestSOAuthorizationScriptMessageHandler alloc] initWithExpectation:@[@"http://www.example.com", @"SOAuthorizationDidStart"]]);
[[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"testHandler"];

auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500) configuration:configuration.get()]);
auto delegate = adoptNS([[TestSOAuthorizationDelegate alloc] init]);
configureSOAuthorizationWebView(webView.get(), delegate.get());

[webView loadHTMLString:testHtml baseURL:nil];
Util::run(&allMessagesReceived);

checkAuthorizationOptions(false, "null"_s, 2);
EXPECT_TRUE(policyForAppSSOPerformed);

[messageHandler extendExpectations:@[@"http://www.example.com", @"SOAuthorizationDidCancel"]];

auto response = adoptNS([[NSHTTPURLResponse alloc] initWithURL:testURL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:@{ @"Set-Cookie" : @"sessionid=38afes7a8;", @"X-Frame-Options" : @"DENY" }]);
auto iframeHtmlCString = generateHtml(iframeTemplate, "parent.postMessage('Cookies: ' + document.cookie, '*');"_s).utf8();
[gDelegate authorization:gAuthorization didCompleteWithHTTPResponse:response.get() httpBody:adoptNS([[NSData alloc] initWithBytes:iframeHtmlCString.data() length:iframeHtmlCString.length()]).get()];
Util::run(&allMessagesReceived);
}
Expand Down

0 comments on commit 70c1251

Please sign in to comment.