Skip to content

Commit c82619c

Browse files
committed
CSP: Port upgrade matching should only apply to insecure source schemes
https://bugs.webkit.org/show_bug.cgi?id=308747 rdar://171265255 Reviewed by Brent Fulgham. A CSP source expression such as "frame-src https://host:80" incorrectly matches https://host (port 443). The mismatch (80 ≠ 443) should cause a block, but WebKit has scheme upgrade logic that treats default ports as equivalent during http-to-https transitions. This logic does not verify the source scheme is insecure, so it treats any source with port 80 as upgradable — even when the source scheme is already HTTPS. Have schemeMatches() return a SchemeMatchResult enum that distinguishes exact matches from insecure-to-secure upgrades, and pass this to portMatches() so the upgrade path only fires when a scheme upgrade is actually occurring. This also fixes an edge case with schemeless source expressions. Both functions are annotated with CSP3 spec step references. * LayoutTests/http/tests/security/contentSecurityPolicy/script-src-parsing-implicit-and-explicit-port-number-expected.txt: * LayoutTests/http/tests/security/contentSecurityPolicy/script-src-parsing-implicit-and-explicit-port-number.html: * LayoutTests/platform/wk2/TestExpectations: * Source/WebCore/page/csp/ContentSecurityPolicySource.cpp: (WebCore::ContentSecurityPolicySource::portMatches const): Canonical link: https://commits.webkit.org/311148@main
1 parent 723bdac commit c82619c

5 files changed

Lines changed: 106 additions & 31 deletions

File tree

LayoutTests/http/tests/security/contentSecurityPolicy/script-src-parsing-implicit-and-explicit-port-number-expected.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
CONSOLE MESSAGE: Refused to load https://127.0.0.1:8443/security/contentSecurityPolicy/resources/script.js because it does not appear in the script-src directive of the Content Security Policy.
2+
CONSOLE MESSAGE: Refused to load https://127.0.0.1:8443/security/contentSecurityPolicy/resources/script.js because it does not appear in the script-src directive of the Content Security Policy.
13
Tests script-src source expression matching with implicit and explicit default port numbers.
24

35

@@ -26,3 +28,28 @@ PASS
2628
Frame: '<!--frame5-->'
2729
--------
2830
PASS
31+
32+
--------
33+
Frame: '<!--frame6-->'
34+
--------
35+
PASS
36+
37+
--------
38+
Frame: '<!--frame7-->'
39+
--------
40+
PASS
41+
42+
--------
43+
Frame: '<!--frame8-->'
44+
--------
45+
PASS
46+
47+
--------
48+
Frame: '<!--frame9-->'
49+
--------
50+
PASS
51+
52+
--------
53+
Frame: '<!--frame10-->'
54+
--------
55+
PASS

LayoutTests/http/tests/security/contentSecurityPolicy/script-src-parsing-implicit-and-explicit-port-number.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@
1919

2020
// Tests that HTTPS URL with explicit default port number matches 'self'.
2121
["yes", "script-src 'self'", "https://127.0.0.1:8443/security/contentSecurityPolicy/resources/script.js"],
22+
23+
// Tests that HTTPS URL does not match HTTPS source expression with opposite scheme's default port number.
24+
["no", "script-src https://127.0.0.1:8000", "https://127.0.0.1:8443/security/contentSecurityPolicy/resources/script.js"],
25+
26+
// Tests that HTTPS URL matches HTTP source expression with HTTPS default port number (upgrade with matching port).
27+
["yes", "script-src http://127.0.0.1:8443", "https://127.0.0.1:8443/security/contentSecurityPolicy/resources/script.js"],
28+
29+
// Tests that HTTPS URL matches WS source expression with HTTP default port (ws scheme upgrade with port upgrade).
30+
["yes", "script-src ws://127.0.0.1:8000", "https://127.0.0.1:8443/security/contentSecurityPolicy/resources/script.js"],
31+
32+
// Tests that HTTPS URL matches schemeless source expression with HTTP default port on HTTP page.
33+
["yes", "script-src 127.0.0.1:8000", "https://127.0.0.1:8443/security/contentSecurityPolicy/resources/script.js"],
34+
35+
// Tests that HTTPS URL does not match WSS source expression with HTTP default port (secure scheme, no port upgrade).
36+
["no", "script-src wss://127.0.0.1:8000", "https://127.0.0.1:8443/security/contentSecurityPolicy/resources/script.js"],
2237
];
2338
</script>
2439
</head>

LayoutTests/platform/wk2/TestExpectations

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,6 @@ fast/preloader/document-write-2.html [ Pass Failure ]
420420

421421
# Internals.registerDefaultPortForProtocol() does not affect NetworkProcess. We should
422422
# look to remove it and write these test to make use of an HTTP server running on port 80.
423-
http/tests/security/contentSecurityPolicy/script-src-parsing-implicit-and-explicit-port-number.html
424423
http/tests/security/http-0.9/default-port-script-blocked.html
425424
http/tests/security/http-0.9/image-default-port-allowed.html
426425
http/tests/security/http-0.9/image-on-HTTP-0.9-default-port-page-allowed-ref-test.html

Source/WebCore/page/csp/ContentSecurityPolicySource.cpp

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -52,36 +52,54 @@ ContentSecurityPolicySource::ContentSecurityPolicySource(const ContentSecurityPo
5252
bool ContentSecurityPolicySource::matches(const URL& url, bool didReceiveRedirectResponse) const
5353
{
5454
// https://www.w3.org/TR/CSP3/#match-url-to-source-expression.
55-
if (!schemeMatches(url))
55+
auto schemeMatch = schemeMatches(url);
56+
if (schemeMatch == SchemeMatchResult::NoMatch)
5657
return false;
5758
if (isSchemeOnly())
5859
return true;
59-
return hostMatches(url) && portMatches(url) && (didReceiveRedirectResponse || pathMatches(url));
60+
auto shouldUpgradePorts = (schemeMatch == SchemeMatchResult::InsecureUpgradeMatch) ? ShouldUpgradePorts::Yes : ShouldUpgradePorts::No;
61+
return hostMatches(url) && portMatches(url, shouldUpgradePorts) && (didReceiveRedirectResponse || pathMatches(url));
6062
}
6163

62-
bool ContentSecurityPolicySource::schemeMatches(const URL& url) const
64+
SchemeMatchResult ContentSecurityPolicySource::schemeMatches(const URL& url) const
6365
{
6466
// https://www.w3.org/TR/CSP3/#match-schemes.
65-
auto& scheme = m_scheme.isEmpty() ? m_policy->selfProtocol() : m_scheme;
67+
const auto& scheme = m_scheme.isEmpty() ? m_policy->selfProtocol() : m_scheme;
6668
auto urlScheme = url.protocol();
6769

70+
// Step 1.1: A matches B.
6871
if (scheme == urlScheme)
69-
return true;
72+
return SchemeMatchResult::Match;
7073

71-
// host-sources can do direct-upgrades.
74+
// Step 1.2: A is "http" and B is "https".
7275
if (scheme == "http"_s && urlScheme == "https"_s)
73-
return true;
74-
if (scheme == "ws"_s && (urlScheme == "wss"_s || urlScheme == "https"_s || urlScheme == "http"_s))
75-
return true;
76-
if (scheme == "wss"_s && urlScheme == "https"_s)
77-
return true;
76+
return SchemeMatchResult::InsecureUpgradeMatch;
77+
78+
// Step 1.3: A is "ws" and B is "wss", "http", or "https".
79+
if (scheme == "ws"_s) {
80+
if (urlScheme == "wss"_s || urlScheme == "https"_s)
81+
return SchemeMatchResult::InsecureUpgradeMatch;
82+
if (urlScheme == "http"_s)
83+
return SchemeMatchResult::Match;
84+
}
7885

79-
// self-sources can always upgrade to secure protocols and side-grade insecure protocols.
80-
if ((m_isSelfSource
81-
&& ((urlScheme == "https"_s || urlScheme == "wss"_s) || (scheme == "http"_s && urlScheme == "ws"_s))))
82-
return true;
86+
// Step 1.4: A is "wss" and B is "https".
87+
if (scheme == "wss"_s && urlScheme == "https"_s)
88+
return SchemeMatchResult::Match;
89+
90+
// self-sources can always upgrade to secure protocols and side-grade insecure protocols. (§6.7.2.8 step 4 NOTE)
91+
if (m_isSelfSource) {
92+
if (urlScheme == "https"_s || urlScheme == "wss"_s) {
93+
if (scheme == "http"_s || scheme == "ws"_s)
94+
return SchemeMatchResult::InsecureUpgradeMatch;
95+
return SchemeMatchResult::Match;
96+
}
97+
if (scheme == "http"_s && urlScheme == "ws"_s)
98+
return SchemeMatchResult::Match;
99+
}
83100

84-
return false;
101+
// Step 2: return "Does Not Match".
102+
return SchemeMatchResult::NoMatch;
85103
}
86104

87105
static bool NODELETE wildcardMatches(StringView host, const String& hostWithWildcard)
@@ -117,30 +135,44 @@ bool ContentSecurityPolicySource::pathMatches(const URL& url) const
117135
return path == m_path;
118136
}
119137

120-
bool ContentSecurityPolicySource::portMatches(const URL& url) const
138+
bool ContentSecurityPolicySource::portMatches(const URL& url, ShouldUpgradePorts shouldUpgradePorts) const
121139
{
140+
// https://www.w3.org/TR/CSP3/#match-ports
141+
// input is the source expression's port-part (m_port).
142+
// url is the URL being checked against the policy.
143+
144+
// Step 1: Assert input is null, "*", or a sequence of one or more ASCII digits.
145+
ASSERT(!(m_portHasWildcard && m_port));
146+
147+
// Step 2: wildcard port matches any URL.
122148
if (m_portHasWildcard)
123149
return true;
124150

125-
std::optional<uint16_t> port = url.port();
151+
std::optional<uint16_t> urlPort = url.port();
126152

127-
if (port == m_port)
153+
// Steps 3-4: if normalizedInput equals url's port (including null), return "Matches".
154+
if (urlPort == m_port)
128155
return true;
129156

130-
// host-source and self-source allows upgrading to a more secure scheme which allows for different ports.
131-
auto defaultSecurePort = WTF::defaultPortForProtocol("https"_s).value_or(443);
132-
auto defaultInsecurePort = WTF::defaultPortForProtocol("http"_s).value_or(80);
133-
bool isUpgradeSecure = (port == defaultSecurePort) || (!port && (url.protocol() == "https"_s || url.protocol() == "wss"_s));
134-
bool isCurrentUpgradable = (m_port == defaultInsecurePort) || (m_scheme == "http"_s && (!m_port || m_port == defaultSecurePort));
135-
if (isUpgradeSecure && isCurrentUpgradable)
136-
return true;
157+
// When upgrading from an insecure to secure scheme, treat default
158+
// ports as equivalent since they differ across schemes (80 vs 443).
159+
if (shouldUpgradePorts == ShouldUpgradePorts::Yes) {
160+
auto defaultSecurePort = WTF::defaultPortForProtocol("https"_s).value_or(443);
161+
auto defaultInsecurePort = WTF::defaultPortForProtocol("http"_s).value_or(80);
162+
bool urlOnSecureDefaultPort = (urlPort == defaultSecurePort) || (!urlPort && (url.protocol() == "https"_s || url.protocol() == "wss"_s));
163+
bool sourcePortIsUpgradable = !m_port || m_port == defaultInsecurePort || m_port == defaultSecurePort;
164+
if (urlOnSecureDefaultPort && sourcePortIsUpgradable)
165+
return true;
166+
}
137167

138-
if (!port)
168+
// Step 5: if url's port is null, check if source port is the default for url's scheme.
169+
if (!urlPort)
139170
return WTF::isDefaultPortForProtocol(m_port.value(), url.protocol());
140171

141172
if (!m_port)
142-
return WTF::isDefaultPortForProtocol(port.value(), url.protocol());
173+
return WTF::isDefaultPortForProtocol(urlPort.value(), url.protocol());
143174

175+
// Step 6: return "Does Not Match".
144176
return false;
145177
}
146178

Source/WebCore/page/csp/ContentSecurityPolicySource.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class ContentSecurityPolicy;
3636
class SecurityOriginData;
3737

3838
enum class IsSelfSource : bool { No, Yes };
39+
enum class SchemeMatchResult : uint8_t { NoMatch, Match, InsecureUpgradeMatch };
40+
enum class ShouldUpgradePorts : bool { No, Yes };
3941

4042
class ContentSecurityPolicySource {
4143
WTF_MAKE_TZONE_ALLOCATED(ContentSecurityPolicySource);
@@ -47,10 +49,10 @@ class ContentSecurityPolicySource {
4749
operator SecurityOriginData() const;
4850

4951
private:
50-
bool schemeMatches(const URL&) const;
52+
SchemeMatchResult schemeMatches(const URL&) const;
5153
bool NODELETE hostMatches(const URL&) const;
5254
bool pathMatches(const URL&) const;
53-
bool portMatches(const URL&) const;
55+
bool portMatches(const URL&, ShouldUpgradePorts) const;
5456
bool NODELETE isSchemeOnly() const;
5557

5658
const CheckedRef<const ContentSecurityPolicy> m_policy;

0 commit comments

Comments
 (0)