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