OLD | NEW |
(Empty) | |
| 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 <algorithm> |
| 6 #include <sstream> |
| 7 |
| 8 #include "base/strings/string_split.h" |
| 9 #include "base/strings/string_util.h" |
| 10 #include "base/strings/utf_string_conversions.h" |
| 11 #include "content/common/content_security_policy/csp_context.h" |
| 12 #include "url/url_canon.h" |
| 13 #include "url/url_util.h" |
| 14 |
| 15 namespace content { |
| 16 |
| 17 namespace { |
| 18 bool IsHostCharacter(char c) { |
| 19 return base::IsAsciiAlpha(c) || base::IsAsciiDigit(c) || c == '-'; |
| 20 } |
| 21 |
| 22 bool IsSchemeContinuationCharacter(char c) { |
| 23 return base::IsAsciiAlpha(c) || base::IsAsciiDigit(c) || c == '-' || |
| 24 c == '+' || c == '.'; |
| 25 } |
| 26 |
| 27 bool DecodePath(const base::StringPiece& path, std::string* output) { |
| 28 url::RawCanonOutputT<base::char16> unescaped; |
| 29 url::DecodeURLEscapeSequences(path.data(), path.size(), &unescaped); |
| 30 return base::UTF16ToUTF8(unescaped.data(), unescaped.length(), output); |
| 31 } |
| 32 |
| 33 int DefaultPortForScheme(const std::string& scheme) { |
| 34 return url::DefaultPortForScheme(scheme.data(), scheme.size()); |
| 35 } |
| 36 |
| 37 // ; <scheme> production from RFC 3986 |
| 38 // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) |
| 39 bool ParseScheme(CSPSource* csp_source, const base::StringPiece& scheme) { |
| 40 std::string scheme_lower = base::ToLowerASCII(scheme); |
| 41 |
| 42 if (scheme.empty()) |
| 43 return false; |
| 44 |
| 45 if (!base::IsAsciiAlpha(scheme[0]) || |
| 46 !std::all_of(scheme.begin() + 1, scheme.end(), |
| 47 IsSchemeContinuationCharacter)) |
| 48 return false; |
| 49 |
| 50 csp_source->scheme = scheme.as_string(); |
| 51 return true; |
| 52 } |
| 53 |
| 54 // host = [ "*." ] 1*host-char *( "." 1*host-char ) |
| 55 // / "*" |
| 56 // host-char = ALPHA / DIGIT / "-" |
| 57 // |
| 58 bool ParseHost(CSPSource* csp_source, const base::StringPiece& host) { |
| 59 if (host.empty()) |
| 60 return false; |
| 61 |
| 62 // Parse and skip leading "*." |
| 63 if (host[0] == '*') { |
| 64 csp_source->is_host_wildcard = true; |
| 65 if (host.size() == 1) |
| 66 return true; |
| 67 |
| 68 if (host[1] != '.') |
| 69 return false; |
| 70 |
| 71 csp_source->host = host.substr(2, std::string::npos).as_string(); |
| 72 |
| 73 // Check the '*.' case. |
| 74 if (csp_source->host.empty()) |
| 75 return false; |
| 76 |
| 77 } else { |
| 78 csp_source->host = host.as_string(); |
| 79 } |
| 80 |
| 81 for (const base::StringPiece& piece : |
| 82 base::SplitStringPiece(csp_source->host, ".", base::KEEP_WHITESPACE, |
| 83 base::SPLIT_WANT_ALL)) { |
| 84 if (piece.empty() || |
| 85 !std::all_of(piece.begin(), piece.end(), IsHostCharacter)) |
| 86 return false; |
| 87 } |
| 88 |
| 89 return true; |
| 90 } |
| 91 |
| 92 // port = 1*DIGIT |
| 93 // / "*" |
| 94 bool ParsePort(CSPSource* csp_source, const base::StringPiece& port) { |
| 95 if (port.empty()) |
| 96 return false; |
| 97 |
| 98 if (port == "*") { |
| 99 csp_source->is_port_wildcard = true; |
| 100 return true; |
| 101 } |
| 102 |
| 103 std::string port_as_string = port.as_string(); |
| 104 if (!std::all_of(port_as_string.begin(), port_as_string.end(), |
| 105 base::IsAsciiDigit<char>)) |
| 106 return false; |
| 107 |
| 108 csp_source->port = std::stoi(port.as_string()); |
| 109 return true; |
| 110 } |
| 111 |
| 112 bool ParsePath(CSPSource* source, const base::StringPiece& path) { |
| 113 DCHECK(base::StartsWith(path, "/", base::CompareCase::INSENSITIVE_ASCII)); |
| 114 |
| 115 size_t position = path.find_first_of("?#"); |
| 116 if (position != std::string::npos) { |
| 117 // path/to/file.js?query=string || path/to/file.js#anchor |
| 118 // ^ ^ |
| 119 // TODO(arthursonzogni): Report that the part after {%,?} will be ignored. |
| 120 } |
| 121 |
| 122 return DecodePath(path.substr(0, position), &(source->path)); |
| 123 } |
| 124 |
| 125 bool ParseSource(CSPSource* csp_source, const std::string& source) { |
| 126 size_t begin = 0; |
| 127 size_t position = 0; |
| 128 position = source.find_first_of(":/", begin); |
| 129 |
| 130 if (position == std::string::npos) { |
| 131 // host |
| 132 // ^ |
| 133 return ParseHost(csp_source, source); |
| 134 } |
| 135 |
| 136 if (source[position] == '/') { |
| 137 // host/path |
| 138 // ^ |
| 139 return ParseHost(csp_source, source.substr(begin, position - begin)) && |
| 140 ParsePath(csp_source, source.substr(position, std::string::npos)); |
| 141 } |
| 142 |
| 143 if (source[position] == ':') { |
| 144 if (position == source.size() - 1) { |
| 145 // scheme: |
| 146 // ^ |
| 147 return ParseScheme(csp_source, source.substr(begin, position - begin)); |
| 148 } |
| 149 |
| 150 if (source[position + 1] == '/') { |
| 151 // scheme://(.*) |
| 152 // ^ ^ |
| 153 if (!ParseScheme(csp_source, source.substr(begin, position - begin))) |
| 154 return false; |
| 155 |
| 156 // check the presence of "://" |
| 157 if (position + 3 >= source.size() || source.substr(position, 3) != "://") |
| 158 return false; |
| 159 |
| 160 if (position + 3 == source.size()) { |
| 161 // scheme:// |
| 162 // ^ |
| 163 return ParseScheme(csp_source, source.substr(begin, position - begin)); |
| 164 } |
| 165 |
| 166 // Skip scheme:// |
| 167 begin = position + 3; |
| 168 position = source.find_first_of(":/", begin); |
| 169 if (position == std::string::npos) { |
| 170 // host |
| 171 return ParseHost(csp_source, source.substr(begin, std::string::npos)); |
| 172 } |
| 173 } |
| 174 } |
| 175 |
| 176 // At this point, the scheme part (if any) has been Skipped. |
| 177 // host/path || host:port[/path] |
| 178 // ^ ^ |
| 179 if (!ParseHost(csp_source, source.substr(begin, position - begin))) |
| 180 return false; |
| 181 |
| 182 if (source[position] == '/') { |
| 183 // host/path |
| 184 // ^ |
| 185 return ParsePath(csp_source, source.substr(position, std::string::npos)); |
| 186 } else { |
| 187 // host:port[/path] |
| 188 // ^ |
| 189 DCHECK(source[position] == ':'); |
| 190 |
| 191 // Skip host: |
| 192 begin = position + 1; |
| 193 position = source.find_first_of("/", begin); |
| 194 |
| 195 if (!ParsePort(csp_source, source.substr(begin, position - begin))) |
| 196 return false; |
| 197 |
| 198 if (position == std::string::npos) |
| 199 return true; |
| 200 |
| 201 // port/path |
| 202 // ^ |
| 203 return ParsePath(csp_source, source.substr(position, std::string::npos)); |
| 204 } |
| 205 } |
| 206 |
| 207 } // namespace |
| 208 |
| 209 CSPSource::CSPSource() |
| 210 : scheme(), |
| 211 host(), |
| 212 is_host_wildcard(false), |
| 213 port(url::PORT_UNSPECIFIED), |
| 214 is_port_wildcard(false), |
| 215 path() {} |
| 216 |
| 217 CSPSource::CSPSource(const std::string& scheme, |
| 218 const std::string& host, |
| 219 bool is_host_wildcard, |
| 220 int port, |
| 221 int is_port_wildcard, |
| 222 const std::string& path) |
| 223 : scheme(scheme), |
| 224 host(host), |
| 225 is_host_wildcard(is_host_wildcard), |
| 226 port(port), |
| 227 is_port_wildcard(is_port_wildcard), |
| 228 path(path) { |
| 229 DCHECK(!has_port() || has_host()); // port => host |
| 230 DCHECK(!has_path() || has_host()); // path => host |
| 231 DCHECK(!is_port_wildcard || port == url::PORT_UNSPECIFIED); |
| 232 } |
| 233 |
| 234 CSPSource::CSPSource(const CSPSource& source) = default; |
| 235 CSPSource::~CSPSource() = default; |
| 236 |
| 237 // source = scheme ":" |
| 238 // / ( [ scheme "://" ] host [ port ] [ path ] ) |
| 239 // / "'self'" |
| 240 // static |
| 241 base::Optional<CSPSource> CSPSource::Parse(const std::string& text) { |
| 242 CSPSource csp_source; |
| 243 if (ParseSource(&csp_source, text)) |
| 244 return csp_source; |
| 245 else |
| 246 return {}; |
| 247 } |
| 248 |
| 249 bool CSPSource::Allow(CSPContext* context, |
| 250 const GURL& url, |
| 251 bool is_redirect) const { |
| 252 if (IsSchemeOnly()) |
| 253 return AllowScheme(url, context); |
| 254 else |
| 255 return AllowScheme(url, context) && AllowHost(url) && AllowPort(url) && |
| 256 AllowPath(url, is_redirect); |
| 257 } |
| 258 |
| 259 std::string CSPSource::ToString() const { |
| 260 // scheme |
| 261 if (IsSchemeOnly()) |
| 262 return scheme; |
| 263 |
| 264 std::stringstream text; |
| 265 if (!scheme.empty()) |
| 266 text << scheme << "://"; |
| 267 |
| 268 // host |
| 269 if (is_host_wildcard) { |
| 270 if (host.empty()) |
| 271 text << "*"; |
| 272 else |
| 273 text << "*." << host; |
| 274 } else { |
| 275 text << host; |
| 276 } |
| 277 |
| 278 // port |
| 279 if (is_port_wildcard) |
| 280 text << ":*"; |
| 281 if (port != url::PORT_UNSPECIFIED) |
| 282 text << ":" << port; |
| 283 |
| 284 // path |
| 285 text << path; |
| 286 |
| 287 return text.str(); |
| 288 } |
| 289 |
| 290 bool CSPSource::IsSchemeOnly() const { |
| 291 return !has_host(); |
| 292 } |
| 293 |
| 294 bool CSPSource::has_port() const { |
| 295 return port != url::PORT_UNSPECIFIED || is_port_wildcard; |
| 296 } |
| 297 |
| 298 bool CSPSource::has_host() const { |
| 299 return !host.empty() || is_host_wildcard; |
| 300 } |
| 301 |
| 302 bool CSPSource::has_path() const { |
| 303 return !path.empty(); |
| 304 } |
| 305 |
| 306 bool CSPSource::AllowScheme(const GURL& url, CSPContext* context) const { |
| 307 if (scheme.empty()) |
| 308 return context->ProtocolMatchesSelf(url); |
| 309 if (scheme == url::kHttpScheme) |
| 310 return url.SchemeIsHTTPOrHTTPS(); |
| 311 if (scheme == url::kWsScheme) |
| 312 return url.SchemeIsWSOrWSS(); |
| 313 return url.SchemeIs(scheme); |
| 314 } |
| 315 |
| 316 bool CSPSource::AllowHost(const GURL& url) const { |
| 317 if (is_host_wildcard) { |
| 318 if (host.empty()) |
| 319 return true; |
| 320 // TODO(arthursonzogni): Chrome used to, incorrectly, match *.x.y to x.y. |
| 321 // The renderer version of this function count how many times it happens. |
| 322 // See third_party/WebKit/Source/core/frame/csp/CSPSource.cpp |
| 323 return base::EndsWith(url.host(), '.' + host, |
| 324 base::CompareCase::INSENSITIVE_ASCII); |
| 325 } else |
| 326 return url.host() == host; |
| 327 } |
| 328 |
| 329 bool CSPSource::AllowPort(const GURL& url) const { |
| 330 int url_port = url.EffectiveIntPort(); |
| 331 |
| 332 if (is_port_wildcard) |
| 333 return true; |
| 334 |
| 335 if (port == url::PORT_UNSPECIFIED) |
| 336 return DefaultPortForScheme(url.scheme()) == url_port; |
| 337 |
| 338 if (port == url_port) |
| 339 return true; |
| 340 |
| 341 if (port == 80 && url_port == 443) |
| 342 return true; |
| 343 |
| 344 return false; |
| 345 } |
| 346 |
| 347 bool CSPSource::AllowPath(const GURL& url, bool is_redirect) const { |
| 348 if (is_redirect) |
| 349 return true; |
| 350 |
| 351 if (path.empty() || url.path().empty()) |
| 352 return true; |
| 353 |
| 354 std::string url_path; |
| 355 if (!DecodePath(url.path(), &url_path)) { |
| 356 // TODO(arthursonzogni). Considering doing something else here. |
| 357 return false; |
| 358 } |
| 359 |
| 360 // If the path represents a directory. |
| 361 if (base::EndsWith(path, "/", base::CompareCase::SENSITIVE)) |
| 362 return base::StartsWith(url_path, path, base::CompareCase::SENSITIVE); |
| 363 |
| 364 // The path represents a file. |
| 365 return path == url_path; |
| 366 } |
| 367 |
| 368 } // namespace content |
OLD | NEW |