1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include <sstream>
6
7 #include "services/network/public/cpp/content_security_policy/csp_source.h"
8
9 #include "base/strings/string_util.h"
10 #include "base/strings/utf_string_conversions.h"
11 #include "services/network/public/cpp/content_security_policy/content_security_policy.h"
12 #include "services/network/public/cpp/content_security_policy/csp_context.h"
13 #include "services/network/public/mojom/content_security_policy.mojom.h"
14 #include "url/url_canon.h"
15 #include "url/url_util.h"
16
17 namespace network {
18
19 namespace {
20
HasHost(const mojom::CSPSourcePtr & source)21 bool HasHost(const mojom::CSPSourcePtr& source) {
22 return !source->host.empty() || source->is_host_wildcard;
23 }
24
DecodePath(const base::StringPiece & path,std::string * output)25 bool DecodePath(const base::StringPiece& path, std::string* output) {
26 url::RawCanonOutputT<base::char16> unescaped;
27 url::DecodeURLEscapeSequences(path.data(), path.size(),
28 url::DecodeURLMode::kUTF8OrIsomorphic,
29 &unescaped);
30 return base::UTF16ToUTF8(unescaped.data(), unescaped.length(), output);
31 }
32
DefaultPortForScheme(const std::string & scheme)33 int DefaultPortForScheme(const std::string& scheme) {
34 return url::DefaultPortForScheme(scheme.data(), scheme.size());
35 }
36
37 // NotMatching is the only negative member, the rest are different types of
38 // matches. NotMatching should always be 0 to let if statements work nicely
39 enum class PortMatchingResult {
40 NotMatching,
41 MatchingWildcard,
42 MatchingUpgrade,
43 MatchingExact
44 };
45 enum class SchemeMatchingResult { NotMatching, MatchingUpgrade, MatchingExact };
46
MatchScheme(const std::string & scheme_a,const std::string & scheme_b)47 SchemeMatchingResult MatchScheme(const std::string& scheme_a,
48 const std::string& scheme_b) {
49 if (scheme_a == scheme_b)
50 return SchemeMatchingResult::MatchingExact;
51 if ((scheme_a == url::kHttpScheme && scheme_b == url::kHttpsScheme) ||
52 (scheme_a == url::kWsScheme && scheme_b == url::kWssScheme)) {
53 return SchemeMatchingResult::MatchingUpgrade;
54 }
55 return SchemeMatchingResult::NotMatching;
56 }
57
SourceAllowScheme(const mojom::CSPSourcePtr & source,const GURL & url,CSPContext * context)58 SchemeMatchingResult SourceAllowScheme(const mojom::CSPSourcePtr& source,
59 const GURL& url,
60 CSPContext* context) {
61 // The source doesn't specify a scheme and the current origin is unique. In
62 // this case, the url doesn't match regardless of its scheme.
63 if (source->scheme.empty() && !context->self_source())
64 return SchemeMatchingResult::NotMatching;
65
66 // |allowed_scheme| is guaranteed to be non-empty.
67 const std::string& allowed_scheme =
68 source->scheme.empty() ? context->self_source()->scheme : source->scheme;
69
70 return MatchScheme(allowed_scheme, url.scheme());
71 }
72
SourceAllowHost(const mojom::CSPSourcePtr & source,const std::string & host)73 bool SourceAllowHost(const mojom::CSPSourcePtr& source,
74 const std::string& host) {
75 if (source->is_host_wildcard) {
76 if (source->host.empty())
77 return true;
78 // TODO(arthursonzogni): Chrome used to, incorrectly, match *.x.y to x.y.
79 // The renderer version of this function counts how many times it happens.
80 // It might be useful to do it outside of blink too.
81 // See third_party/blink/renderer/core/frame/csp/csp_source.cc
82 return base::EndsWith(host, '.' + source->host,
83 base::CompareCase::INSENSITIVE_ASCII);
84 } else {
85 return base::EqualsCaseInsensitiveASCII(host, source->host);
86 }
87 }
88
SourceAllowHost(const mojom::CSPSourcePtr & source,const GURL & url)89 bool SourceAllowHost(const mojom::CSPSourcePtr& source, const GURL& url) {
90 return SourceAllowHost(source, url.host());
91 }
92
SourceAllowPort(const mojom::CSPSourcePtr & source,int port,const std::string & scheme)93 PortMatchingResult SourceAllowPort(const mojom::CSPSourcePtr& source,
94 int port,
95 const std::string& scheme) {
96 if (source->is_port_wildcard)
97 return PortMatchingResult::MatchingWildcard;
98
99 if (source->port == port) {
100 if (source->port == url::PORT_UNSPECIFIED)
101 return PortMatchingResult::MatchingWildcard;
102 return PortMatchingResult::MatchingExact;
103 }
104
105 if (source->port == url::PORT_UNSPECIFIED) {
106 if (DefaultPortForScheme(scheme) == port)
107 return PortMatchingResult::MatchingWildcard;
108 }
109
110 if (port == url::PORT_UNSPECIFIED) {
111 if (source->port == DefaultPortForScheme(scheme))
112 return PortMatchingResult::MatchingWildcard;
113 }
114
115 int source_port = source->port;
116 if (source_port == url::PORT_UNSPECIFIED)
117 source_port = DefaultPortForScheme(source->scheme);
118
119 if (port == url::PORT_UNSPECIFIED)
120 port = DefaultPortForScheme(scheme);
121
122 if (source_port == 80 && port == 443)
123 return PortMatchingResult::MatchingUpgrade;
124
125 return PortMatchingResult::NotMatching;
126 }
127
SourceAllowPort(const mojom::CSPSourcePtr & source,const GURL & url)128 PortMatchingResult SourceAllowPort(const mojom::CSPSourcePtr& source,
129 const GURL& url) {
130 return SourceAllowPort(source, url.EffectiveIntPort(), url.scheme());
131 }
132
SourceAllowPath(const mojom::CSPSourcePtr & source,const std::string & path)133 bool SourceAllowPath(const mojom::CSPSourcePtr& source,
134 const std::string& path) {
135 std::string path_decoded;
136 if (!DecodePath(path, &path_decoded)) {
137 // TODO(arthursonzogni): try to figure out if that could happen and how to
138 // handle it.
139 return false;
140 }
141
142 if (source->path.empty() || (source->path == "/" && path_decoded.empty()))
143 return true;
144
145 // If the path represents a directory.
146 if (base::EndsWith(source->path, "/", base::CompareCase::SENSITIVE)) {
147 return base::StartsWith(path_decoded, source->path,
148 base::CompareCase::SENSITIVE);
149 }
150
151 // The path represents a file.
152 return source->path == path_decoded;
153 }
154
SourceAllowPath(const mojom::CSPSourcePtr & source,const GURL & url,bool has_followed_redirect)155 bool SourceAllowPath(const mojom::CSPSourcePtr& source,
156 const GURL& url,
157 bool has_followed_redirect) {
158 if (has_followed_redirect)
159 return true;
160
161 return SourceAllowPath(source, url.path());
162 }
163
requiresUpgrade(const PortMatchingResult result)164 bool requiresUpgrade(const PortMatchingResult result) {
165 return result == PortMatchingResult::MatchingUpgrade;
166 }
167
requiresUpgrade(const SchemeMatchingResult result)168 bool requiresUpgrade(const SchemeMatchingResult result) {
169 return result == SchemeMatchingResult::MatchingUpgrade;
170 }
171
canUpgrade(const PortMatchingResult result)172 bool canUpgrade(const PortMatchingResult result) {
173 return result == PortMatchingResult::MatchingUpgrade ||
174 result == PortMatchingResult::MatchingWildcard;
175 }
176
canUpgrade(const SchemeMatchingResult result)177 bool canUpgrade(const SchemeMatchingResult result) {
178 return result == SchemeMatchingResult::MatchingUpgrade;
179 }
180
181 } // namespace
182
CSPSourceIsSchemeOnly(const mojom::CSPSourcePtr & source)183 bool CSPSourceIsSchemeOnly(const mojom::CSPSourcePtr& source) {
184 return !HasHost(source);
185 }
186
CheckCSPSource(const mojom::CSPSourcePtr & source,const GURL & url,CSPContext * context,bool has_followed_redirect)187 bool CheckCSPSource(const mojom::CSPSourcePtr& source,
188 const GURL& url,
189 CSPContext* context,
190 bool has_followed_redirect) {
191 if (CSPSourceIsSchemeOnly(source)) {
192 return SourceAllowScheme(source, url, context) !=
193 SchemeMatchingResult::NotMatching;
194 }
195 PortMatchingResult portResult = SourceAllowPort(source, url);
196 SchemeMatchingResult schemeResult = SourceAllowScheme(source, url, context);
197 if (requiresUpgrade(schemeResult) && !canUpgrade(portResult))
198 return false;
199 if (requiresUpgrade(portResult) && !canUpgrade(schemeResult))
200 return false;
201 return schemeResult != SchemeMatchingResult::NotMatching &&
202 SourceAllowHost(source, url) &&
203 portResult != PortMatchingResult::NotMatching &&
204 SourceAllowPath(source, url, has_followed_redirect);
205 }
206
CSPSourcesIntersect(const mojom::CSPSourcePtr & source_a,const mojom::CSPSourcePtr & source_b)207 mojom::CSPSourcePtr CSPSourcesIntersect(const mojom::CSPSourcePtr& source_a,
208 const mojom::CSPSourcePtr& source_b) {
209 // If the original source expressions didn't have a scheme, we should have
210 // filled that already with origin's scheme.
211 DCHECK(!source_a->scheme.empty());
212 DCHECK(!source_b->scheme.empty());
213
214 auto result = mojom::CSPSource::New();
215 if (MatchScheme(source_a->scheme, source_b->scheme) !=
216 SchemeMatchingResult::NotMatching) {
217 result->scheme = source_b->scheme;
218 } else if (MatchScheme(source_b->scheme, source_a->scheme) !=
219 SchemeMatchingResult::NotMatching) {
220 result->scheme = source_a->scheme;
221 } else {
222 return nullptr;
223 }
224
225 if (CSPSourceIsSchemeOnly(source_a)) {
226 auto new_result = source_b->Clone();
227 new_result->scheme = result->scheme;
228 return new_result;
229 } else if (CSPSourceIsSchemeOnly(source_b)) {
230 auto new_result = source_a->Clone();
231 new_result->scheme = result->scheme;
232 return new_result;
233 }
234
235 const std::string host_a =
236 (source_a->is_host_wildcard ? "*." : "") + source_a->host;
237 const std::string host_b =
238 (source_b->is_host_wildcard ? "*." : "") + source_b->host;
239 if (SourceAllowHost(source_a, host_b)) {
240 result->host = source_b->host;
241 result->is_host_wildcard = source_b->is_host_wildcard;
242 } else if (SourceAllowHost(source_b, host_a)) {
243 result->host = source_a->host;
244 result->is_host_wildcard = source_a->is_host_wildcard;
245 } else {
246 return nullptr;
247 }
248
249 if (source_b->is_port_wildcard) {
250 result->port = source_a->port;
251 result->is_port_wildcard = source_a->is_port_wildcard;
252 } else if (source_a->is_port_wildcard) {
253 result->port = source_b->port;
254 } else if (SourceAllowPort(source_a, source_b->port, source_b->scheme) !=
255 PortMatchingResult::NotMatching &&
256 // If port_a is explicitly specified but port_b is omitted, then we
257 // should take port_a instead of port_b, since port_a is stricter.
258 !(source_a->port != url::PORT_UNSPECIFIED &&
259 source_b->port == url::PORT_UNSPECIFIED)) {
260 result->port = source_b->port;
261 } else if (SourceAllowPort(source_b, source_a->port, source_a->scheme) !=
262 PortMatchingResult::NotMatching) {
263 result->port = source_a->port;
264 } else {
265 return nullptr;
266 }
267
268 if (SourceAllowPath(source_a, source_b->path))
269 result->path = source_b->path;
270 else if (SourceAllowPath(source_b, source_a->path))
271 result->path = source_a->path;
272 else
273 return nullptr;
274
275 return result;
276 }
277
278 // Check whether |source_a| subsumes |source_b|.
CSPSourceSubsumes(const mojom::CSPSourcePtr & source_a,const mojom::CSPSourcePtr & source_b)279 bool CSPSourceSubsumes(const mojom::CSPSourcePtr& source_a,
280 const mojom::CSPSourcePtr& source_b) {
281 // If the original source expressions didn't have a scheme, we should have
282 // filled that already with origin's scheme.
283 DCHECK(!source_a->scheme.empty());
284 DCHECK(!source_b->scheme.empty());
285
286 if (MatchScheme(source_a->scheme, source_b->scheme) ==
287 SchemeMatchingResult::NotMatching) {
288 return false;
289 }
290
291 if (CSPSourceIsSchemeOnly(source_a))
292 return true;
293 if (CSPSourceIsSchemeOnly(source_b))
294 return false;
295
296 if (!SourceAllowHost(source_a, (source_b->is_host_wildcard ? "*." : "") +
297 source_b->host)) {
298 return false;
299 }
300
301 if (source_b->is_port_wildcard && !source_a->is_port_wildcard)
302 return false;
303 PortMatchingResult port_matching =
304 SourceAllowPort(source_a, source_b->port, source_b->scheme);
305 if (port_matching == PortMatchingResult::NotMatching)
306 return false;
307
308 if (!SourceAllowPath(source_a, source_b->path))
309 return false;
310
311 return true;
312 }
313
ToString(const mojom::CSPSourcePtr & source)314 std::string ToString(const mojom::CSPSourcePtr& source) {
315 // scheme
316 if (CSPSourceIsSchemeOnly(source))
317 return source->scheme + ":";
318
319 std::stringstream text;
320 if (!source->scheme.empty())
321 text << source->scheme << "://";
322
323 // host
324 if (source->is_host_wildcard) {
325 if (source->host.empty())
326 text << "*";
327 else
328 text << "*." << source->host;
329 } else {
330 text << source->host;
331 }
332
333 // port
334 if (source->is_port_wildcard)
335 text << ":*";
336 if (source->port != url::PORT_UNSPECIFIED)
337 text << ":" << source->port;
338
339 // path
340 text << source->path;
341
342 return text.str();
343 }
344
345 } // namespace network
346