Move URL from WebCore to WTF
[WebKit-https.git] / Source / WebCore / html / parser / XSSAuditor.cpp
1 /*
2  * Copyright (C) 2011 Adam Barth. All Rights Reserved.
3  * Copyright (C) 2011 Daniel Bates (dbates@intudata.com).
4  * Copyright (C) 2017 Apple Inc. All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
16  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
19  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
23  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26  */
27
28 #include "config.h"
29 #include "XSSAuditor.h"
30
31 #include "DecodeEscapeSequences.h"
32 #include "Document.h"
33 #include "DocumentLoader.h"
34 #include "FormData.h"
35 #include "Frame.h"
36 #include "FrameLoader.h"
37 #include "HTMLDocumentParser.h"
38 #include "HTMLNames.h"
39 #include "HTMLParamElement.h"
40 #include "HTMLParserIdioms.h"
41 #include "SVGNames.h"
42 #include "Settings.h"
43 #include "TextResourceDecoder.h"
44 #include "XLinkNames.h"
45 #include <wtf/ASCIICType.h>
46 #include <wtf/MainThread.h>
47 #include <wtf/NeverDestroyed.h>
48 #include <wtf/text/StringView.h>
49
50 namespace WebCore {
51
52 using namespace HTMLNames;
53
54 static bool isNonCanonicalCharacter(UChar c)
55 {
56     // We remove all non-ASCII characters, including non-printable ASCII characters.
57     //
58     // Note, we don't remove backslashes like PHP stripslashes(), which among other things converts "\\0" to the \0 character.
59     // Instead, we remove backslashes and zeros (since the string "\\0" =(remove backslashes)=> "0"). However, this has the
60     // adverse effect that we remove any legitimate zeros from a string.
61     // We also remove forward-slash, because it is common for some servers to collapse successive path components, eg,
62     // a//b becomes a/b.
63     //
64     // For instance: new String("http://localhost:8000") => new String("http:localhost:8").
65     return (c == '\\' || c == '0' || c == '\0' || c == '/' || c >= 127);
66 }
67
68 static bool isRequiredForInjection(UChar c)
69 {
70     return (c == '\'' || c == '"' || c == '<' || c == '>');
71 }
72
73 static bool isTerminatingCharacter(UChar c)
74 {
75     return (c == '&' || c == '/' || c == '"' || c == '\'' || c == '<' || c == '>' || c == ',');
76 }
77
78 static bool isHTMLQuote(UChar c)
79 {
80     return (c == '"' || c == '\'');
81 }
82
83 static bool isJSNewline(UChar c)
84 {
85     // Per ecma-262 section 7.3 Line Terminators.
86     return (c == '\n' || c == '\r' || c == 0x2028 || c == 0x2029);
87 }
88
89 static bool startsHTMLCommentAt(const String& string, size_t start)
90 {
91     return (start + 3 < string.length() && string[start] == '<' && string[start + 1] == '!' && string[start + 2] == '-' && string[start + 3] == '-');
92 }
93
94 static bool startsSingleLineCommentAt(const String& string, size_t start)
95 {
96     return (start + 1 < string.length() && string[start] == '/' && string[start + 1] == '/');
97 }
98
99 static bool startsMultiLineCommentAt(const String& string, size_t start)
100 {
101     return (start + 1 < string.length() && string[start] == '/' && string[start + 1] == '*');
102 }
103
104 static bool startsOpeningScriptTagAt(const String& string, size_t start)
105 {
106     return start + 6 < string.length() && string[start] == '<'
107         && WTF::toASCIILowerUnchecked(string[start + 1]) == 's'
108         && WTF::toASCIILowerUnchecked(string[start + 2]) == 'c'
109         && WTF::toASCIILowerUnchecked(string[start + 3]) == 'r'
110         && WTF::toASCIILowerUnchecked(string[start + 4]) == 'i'
111         && WTF::toASCIILowerUnchecked(string[start + 5]) == 'p'
112         && WTF::toASCIILowerUnchecked(string[start + 6]) == 't';
113 }
114
115 // If other files need this, we should move this to HTMLParserIdioms.h
116 template<size_t inlineCapacity>
117 bool threadSafeMatch(const Vector<UChar, inlineCapacity>& vector, const QualifiedName& qname)
118 {
119     return equalIgnoringNullity(vector, qname.localName().impl());
120 }
121
122 static bool hasName(const HTMLToken& token, const QualifiedName& name)
123 {
124     return threadSafeMatch(token.name(), name);
125 }
126
127 static bool findAttributeWithName(const HTMLToken& token, const QualifiedName& name, size_t& indexOfMatchingAttribute)
128 {
129     // Notice that we're careful not to ref the StringImpl here because we might be on a background thread.
130     const String& attrName = name.namespaceURI() == XLinkNames::xlinkNamespaceURI ? "xlink:" + name.localName().string() : name.localName().string();
131
132     for (size_t i = 0; i < token.attributes().size(); ++i) {
133         if (equalIgnoringNullity(token.attributes().at(i).name, attrName)) {
134             indexOfMatchingAttribute = i;
135             return true;
136         }
137     }
138     return false;
139 }
140
141 static bool isNameOfInlineEventHandler(const Vector<UChar, 32>& name)
142 {
143     const size_t lengthOfShortestInlineEventHandlerName = 5; // To wit: oncut.
144     if (name.size() < lengthOfShortestInlineEventHandlerName)
145         return false;
146     return name[0] == 'o' && name[1] == 'n';
147 }
148
149 static bool isDangerousHTTPEquiv(const String& value)
150 {
151     String equiv = value.stripWhiteSpace();
152     return equalLettersIgnoringASCIICase(equiv, "refresh") || equalLettersIgnoringASCIICase(equiv, "set-cookie");
153 }
154
155 static inline String decode16BitUnicodeEscapeSequences(const String& string)
156 {
157     // Note, the encoding is ignored since each %u-escape sequence represents a UTF-16 code unit.
158     return decodeEscapeSequences<Unicode16BitEscapeSequence>(string, UTF8Encoding());
159 }
160
161 static inline String decodeStandardURLEscapeSequences(const String& string, const TextEncoding& encoding)
162 {
163     // We use decodeEscapeSequences() instead of decodeURLEscapeSequences() (declared in URL.h) to
164     // avoid platform-specific URL decoding differences (e.g. URLGoogle).
165     return decodeEscapeSequences<URLEscapeSequence>(string, encoding);
166 }
167
168 static String fullyDecodeString(const String& string, const TextEncoding& encoding)
169 {
170     size_t oldWorkingStringLength;
171     String workingString = string;
172     do {
173         oldWorkingStringLength = workingString.length();
174         workingString = decode16BitUnicodeEscapeSequences(decodeStandardURLEscapeSequences(workingString, encoding));
175     } while (workingString.length() < oldWorkingStringLength);
176     workingString.replace('+', ' ');
177     return workingString;
178 }
179
180 static void truncateForSrcLikeAttribute(String& decodedSnippet)
181 {
182     // In HTTP URLs, characters following the first ?, #, or third slash may come from
183     // the page itself and can be merely ignored by an attacker's server when a remote
184     // script or script-like resource is requested. In data URLs, the payload starts at
185     // the first comma, and the first /*, //, or <!-- may introduce a comment. Also
186     // data URLs may use the same string literal tricks as with script content itself.
187     // In either case, content following this may come from the page and may be ignored
188     // when the script is executed. Also, any of these characters may now be represented
189     // by the (enlarged) set of HTML5 entities.
190     // For simplicity, we don't differentiate based on URL scheme, and stop at the first
191     // & (since it might be part of an entity for any of the subsequent punctuation)
192     // the first # or ?, the third slash, or the first slash, <, ', or " once a comma
193     // is seen.
194     int slashCount = 0;
195     bool commaSeen = false;
196     for (size_t currentLength = 0; currentLength < decodedSnippet.length(); ++currentLength) {
197         UChar currentChar = decodedSnippet[currentLength];
198         if (currentChar == '&'
199             || currentChar == '?'
200             || currentChar == '#'
201             || ((currentChar == '/' || currentChar == '\\') && (commaSeen || ++slashCount > 2))
202             || (currentChar == '<' && commaSeen)
203             || (currentChar == '\'' && commaSeen)
204             || (currentChar == '"' && commaSeen)) {
205             decodedSnippet.truncate(currentLength);
206             return;
207         }
208         if (currentChar == ',')
209             commaSeen = true;
210     }
211 }
212
213 static void truncateForScriptLikeAttribute(String& decodedSnippet)
214 {
215     // Beware of trailing characters which came from the page itself, not the
216     // injected vector. Excluding the terminating character covers common cases
217     // where the page immediately ends the attribute, but doesn't cover more
218     // complex cases where there is other page data following the injection.
219     // Generally, these won't parse as JavaScript, so the injected vector
220     // typically excludes them from consideration via a single-line comment or
221     // by enclosing them in a string literal terminated later by the page's own
222     // closing punctuation. Since the snippet has not been parsed, the vector
223     // may also try to introduce these via entities. As a result, we'd like to
224     // stop before the first "//", the first <!--, the first entity, or the first
225     // quote not immediately following the first equals sign (taking whitespace
226     // into consideration). To keep things simpler, we don't try to distinguish
227     // between entity-introducing ampersands vs. other uses, nor do we bother to
228     // check for a second slash for a comment, nor do we bother to check for
229     // !-- following a less-than sign. We stop instead on any ampersand
230     // slash, or less-than sign.
231     size_t position = 0;
232     if ((position = decodedSnippet.find('=')) != notFound
233         && (position = decodedSnippet.find(isNotHTMLSpace, position + 1)) != notFound
234         && (position = decodedSnippet.find(isTerminatingCharacter, isHTMLQuote(decodedSnippet[position]) ? position + 1 : position)) != notFound) {
235         decodedSnippet.truncate(position);
236     }
237 }
238
239 static bool isSemicolonSeparatedAttribute(const HTMLToken::Attribute& attribute)
240 {
241     return threadSafeMatch(attribute.name, SVGNames::valuesAttr);
242 }
243
244 static bool semicolonSeparatedValueContainsJavaScriptURL(StringView semicolonSeparatedValue)
245 {
246     for (auto value : semicolonSeparatedValue.split(';')) {
247         if (WTF::protocolIsJavaScript(value))
248             return true;
249     }
250     return false;
251 }
252
253 XSSAuditor::XSSAuditor()
254     : m_isEnabled(false)
255     , m_xssProtection(XSSProtectionDisposition::Enabled)
256     , m_didSendValidXSSProtectionHeader(false)
257     , m_state(Uninitialized)
258     , m_scriptTagNestingLevel(0)
259     , m_encoding(UTF8Encoding())
260 {
261     // Although tempting to call init() at this point, the various objects
262     // we want to reference might not all have been constructed yet.
263 }
264
265 void XSSAuditor::initForFragment()
266 {
267     ASSERT(isMainThread());
268     ASSERT(m_state == Uninitialized);
269     m_state = Initialized;
270     // When parsing a fragment, we don't enable the XSS auditor because it's
271     // too much overhead.
272     ASSERT(!m_isEnabled);
273 }
274
275 void XSSAuditor::init(Document* document, XSSAuditorDelegate* auditorDelegate)
276 {
277     ASSERT(isMainThread());
278     if (m_state == Initialized)
279         return;
280     ASSERT(m_state == Uninitialized);
281     m_state = Initialized;
282
283     if (RefPtr<Frame> frame = document->frame())
284         m_isEnabled = frame->settings().xssAuditorEnabled();
285
286     if (!m_isEnabled)
287         return;
288
289     m_documentURL = document->url().isolatedCopy();
290
291     // In theory, the Document could have detached from the Frame after the
292     // XSSAuditor was constructed.
293     if (!document->frame()) {
294         m_isEnabled = false;
295         return;
296     }
297
298     if (m_documentURL.isEmpty()) {
299         // The URL can be empty when opening a new browser window or calling window.open("").
300         m_isEnabled = false;
301         return;
302     }
303
304     if (m_documentURL.protocolIsData()) {
305         m_isEnabled = false;
306         return;
307     }
308
309     if (document->decoder())
310         m_encoding = document->decoder()->encoding();
311
312     m_decodedURL = canonicalize(m_documentURL.string(), TruncationStyle::None);
313     if (m_decodedURL.find(isRequiredForInjection) == notFound)
314         m_decodedURL = String();
315
316     if (RefPtr<DocumentLoader> documentLoader = document->frame()->loader().documentLoader()) {
317         String headerValue = documentLoader->response().httpHeaderField(HTTPHeaderName::XXSSProtection);
318         String errorDetails;
319         unsigned errorPosition = 0;
320         String parsedReportURL;
321         URL reportURL;
322         m_xssProtection = parseXSSProtectionHeader(headerValue, errorDetails, errorPosition, parsedReportURL);
323         m_didSendValidXSSProtectionHeader = !headerValue.isNull() && m_xssProtection != XSSProtectionDisposition::Invalid;
324
325         if ((m_xssProtection == XSSProtectionDisposition::Enabled || m_xssProtection == XSSProtectionDisposition::BlockEnabled) && !parsedReportURL.isEmpty()) {
326             reportURL = document->completeURL(parsedReportURL);
327             if (MixedContentChecker::isMixedContent(document->securityOrigin(), reportURL)) {
328                 errorDetails = "insecure reporting URL for secure page";
329                 m_xssProtection = XSSProtectionDisposition::Invalid;
330                 reportURL = URL();
331                 m_didSendValidXSSProtectionHeader = false;
332             }
333         }
334         if (m_xssProtection == XSSProtectionDisposition::Invalid) {
335             document->addConsoleMessage(MessageSource::Security, MessageLevel::Error, "Error parsing header X-XSS-Protection: " + headerValue + ": "  + errorDetails + " at character position " + String::format("%u", errorPosition) + ". The default protections will be applied.");
336             m_xssProtection = XSSProtectionDisposition::Enabled;
337         }
338
339         if (auditorDelegate)
340             auditorDelegate->setReportURL(reportURL.isolatedCopy());
341         RefPtr<FormData> httpBody = documentLoader->originalRequest().httpBody();
342         if (httpBody && !httpBody->isEmpty()) {
343             String httpBodyAsString = httpBody->flattenToString();
344             if (!httpBodyAsString.isEmpty()) {
345                 m_decodedHTTPBody = canonicalize(httpBodyAsString, TruncationStyle::None);
346                 if (m_decodedHTTPBody.find(isRequiredForInjection) == notFound)
347                     m_decodedHTTPBody = String();
348             }
349         }
350     }
351
352     if (m_decodedURL.isEmpty() && m_decodedHTTPBody.isEmpty()) {
353         m_isEnabled = false;
354         return;
355     }
356 }
357
358 std::unique_ptr<XSSInfo> XSSAuditor::filterToken(const FilterTokenRequest& request)
359 {
360     ASSERT(m_state == Initialized);
361     if (!m_isEnabled || m_xssProtection == XSSProtectionDisposition::Disabled)
362         return nullptr;
363
364     bool didBlockScript = false;
365     if (request.token.type() == HTMLToken::StartTag)
366         didBlockScript = filterStartToken(request);
367     else if (m_scriptTagNestingLevel) {
368         if (request.token.type() == HTMLToken::Character)
369             didBlockScript = filterCharacterToken(request);
370         else if (request.token.type() == HTMLToken::EndTag)
371             filterEndToken(request);
372     }
373
374     if (!didBlockScript)
375         return nullptr;
376
377     bool didBlockEntirePage = m_xssProtection == XSSProtectionDisposition::BlockEnabled;
378     return std::make_unique<XSSInfo>(m_documentURL, didBlockEntirePage, m_didSendValidXSSProtectionHeader);
379 }
380
381 bool XSSAuditor::filterStartToken(const FilterTokenRequest& request)
382 {
383     bool didBlockScript = eraseDangerousAttributesIfInjected(request);
384
385     if (hasName(request.token, scriptTag)) {
386         didBlockScript |= filterScriptToken(request);
387         ASSERT(request.shouldAllowCDATA || !m_scriptTagNestingLevel);
388         m_scriptTagNestingLevel++;
389     } else if (hasName(request.token, objectTag))
390         didBlockScript |= filterObjectToken(request);
391     else if (hasName(request.token, paramTag))
392         didBlockScript |= filterParamToken(request);
393     else if (hasName(request.token, embedTag))
394         didBlockScript |= filterEmbedToken(request);
395     else if (hasName(request.token, appletTag))
396         didBlockScript |= filterAppletToken(request);
397     else if (hasName(request.token, iframeTag) || hasName(request.token, frameTag))
398         didBlockScript |= filterFrameToken(request);
399     else if (hasName(request.token, metaTag))
400         didBlockScript |= filterMetaToken(request);
401     else if (hasName(request.token, baseTag))
402         didBlockScript |= filterBaseToken(request);
403     else if (hasName(request.token, formTag))
404         didBlockScript |= filterFormToken(request);
405     else if (hasName(request.token, inputTag))
406         didBlockScript |= filterInputToken(request);
407     else if (hasName(request.token, buttonTag))
408         didBlockScript |= filterButtonToken(request);
409
410     return didBlockScript;
411 }
412
413 void XSSAuditor::filterEndToken(const FilterTokenRequest& request)
414 {
415     ASSERT(m_scriptTagNestingLevel);
416     if (hasName(request.token, scriptTag)) {
417         m_scriptTagNestingLevel--;
418         ASSERT(request.shouldAllowCDATA || !m_scriptTagNestingLevel);
419     }
420 }
421
422 bool XSSAuditor::filterCharacterToken(const FilterTokenRequest& request)
423 {
424     ASSERT(m_scriptTagNestingLevel);
425     if (m_wasScriptTagFoundInRequest && isContainedInRequest(canonicalizedSnippetForJavaScript(request))) {
426         request.token.clear();
427         LChar space = ' ';
428         request.token.appendToCharacter(space); // Technically, character tokens can't be empty.
429         return true;
430     }
431     return false;
432 }
433
434 bool XSSAuditor::filterScriptToken(const FilterTokenRequest& request)
435 {
436     ASSERT(request.token.type() == HTMLToken::StartTag);
437     ASSERT(hasName(request.token, scriptTag));
438
439     m_wasScriptTagFoundInRequest = isContainedInRequest(canonicalizedSnippetForTagName(request));
440
441     bool didBlockScript = false;
442     if (m_wasScriptTagFoundInRequest) {
443         didBlockScript |= eraseAttributeIfInjected(request, srcAttr, WTF::blankURL().string(), TruncationStyle::SrcLikeAttribute);
444         didBlockScript |= eraseAttributeIfInjected(request, SVGNames::hrefAttr, WTF::blankURL().string(), TruncationStyle::SrcLikeAttribute);
445         didBlockScript |= eraseAttributeIfInjected(request, XLinkNames::hrefAttr, WTF::blankURL().string(), TruncationStyle::SrcLikeAttribute);
446     }
447
448     return didBlockScript;
449 }
450
451 bool XSSAuditor::filterObjectToken(const FilterTokenRequest& request)
452 {
453     ASSERT(request.token.type() == HTMLToken::StartTag);
454     ASSERT(hasName(request.token, objectTag));
455
456     bool didBlockScript = false;
457     if (isContainedInRequest(canonicalizedSnippetForTagName(request))) {
458         didBlockScript |= eraseAttributeIfInjected(request, dataAttr, WTF::blankURL().string(), TruncationStyle::SrcLikeAttribute);
459         didBlockScript |= eraseAttributeIfInjected(request, typeAttr);
460         didBlockScript |= eraseAttributeIfInjected(request, classidAttr);
461     }
462     return didBlockScript;
463 }
464
465 bool XSSAuditor::filterParamToken(const FilterTokenRequest& request)
466 {
467     ASSERT(request.token.type() == HTMLToken::StartTag);
468     ASSERT(hasName(request.token, paramTag));
469
470     size_t indexOfNameAttribute;
471     if (!findAttributeWithName(request.token, nameAttr, indexOfNameAttribute))
472         return false;
473
474     const HTMLToken::Attribute& nameAttribute = request.token.attributes().at(indexOfNameAttribute);
475     if (!HTMLParamElement::isURLParameter(String(nameAttribute.value)))
476         return false;
477
478     return eraseAttributeIfInjected(request, valueAttr, WTF::blankURL().string(), TruncationStyle::SrcLikeAttribute);
479 }
480
481 bool XSSAuditor::filterEmbedToken(const FilterTokenRequest& request)
482 {
483     ASSERT(request.token.type() == HTMLToken::StartTag);
484     ASSERT(hasName(request.token, embedTag));
485
486     bool didBlockScript = false;
487     if (isContainedInRequest(canonicalizedSnippetForTagName(request))) {
488         didBlockScript |= eraseAttributeIfInjected(request, codeAttr, String(), TruncationStyle::SrcLikeAttribute);
489         didBlockScript |= eraseAttributeIfInjected(request, srcAttr, WTF::blankURL().string(), TruncationStyle::SrcLikeAttribute);
490         didBlockScript |= eraseAttributeIfInjected(request, typeAttr);
491     }
492     return didBlockScript;
493 }
494
495 bool XSSAuditor::filterAppletToken(const FilterTokenRequest& request)
496 {
497     ASSERT(request.token.type() == HTMLToken::StartTag);
498     ASSERT(hasName(request.token, appletTag));
499
500     bool didBlockScript = false;
501     if (isContainedInRequest(canonicalizedSnippetForTagName(request))) {
502         didBlockScript |= eraseAttributeIfInjected(request, codeAttr, String(), TruncationStyle::SrcLikeAttribute);
503         didBlockScript |= eraseAttributeIfInjected(request, objectAttr);
504     }
505     return didBlockScript;
506 }
507
508 bool XSSAuditor::filterFrameToken(const FilterTokenRequest& request)
509 {
510     ASSERT(request.token.type() == HTMLToken::StartTag);
511     ASSERT(hasName(request.token, iframeTag) || hasName(request.token, frameTag));
512
513     bool didBlockScript = eraseAttributeIfInjected(request, srcdocAttr, String(), TruncationStyle::ScriptLikeAttribute);
514     if (isContainedInRequest(canonicalizedSnippetForTagName(request)))
515         didBlockScript |= eraseAttributeIfInjected(request, srcAttr, String(), TruncationStyle::SrcLikeAttribute);
516
517     return didBlockScript;
518 }
519
520 bool XSSAuditor::filterMetaToken(const FilterTokenRequest& request)
521 {
522     ASSERT(request.token.type() == HTMLToken::StartTag);
523     ASSERT(hasName(request.token, metaTag));
524
525     return eraseAttributeIfInjected(request, http_equivAttr);
526 }
527
528 bool XSSAuditor::filterBaseToken(const FilterTokenRequest& request)
529 {
530     ASSERT(request.token.type() == HTMLToken::StartTag);
531     ASSERT(hasName(request.token, baseTag));
532
533     return eraseAttributeIfInjected(request, hrefAttr);
534 }
535
536 bool XSSAuditor::filterFormToken(const FilterTokenRequest& request)
537 {
538     ASSERT(request.token.type() == HTMLToken::StartTag);
539     ASSERT(hasName(request.token, formTag));
540
541     return eraseAttributeIfInjected(request, actionAttr, WTF::blankURL().string());
542 }
543
544 bool XSSAuditor::filterInputToken(const FilterTokenRequest& request)
545 {
546     ASSERT(request.token.type() == HTMLToken::StartTag);
547     ASSERT(hasName(request.token, inputTag));
548
549     return eraseAttributeIfInjected(request, formactionAttr, WTF::blankURL().string(), TruncationStyle::SrcLikeAttribute);
550 }
551
552 bool XSSAuditor::filterButtonToken(const FilterTokenRequest& request)
553 {
554     ASSERT(request.token.type() == HTMLToken::StartTag);
555     ASSERT(hasName(request.token, buttonTag));
556
557     return eraseAttributeIfInjected(request, formactionAttr, WTF::blankURL().string(), TruncationStyle::SrcLikeAttribute);
558 }
559
560 bool XSSAuditor::eraseDangerousAttributesIfInjected(const FilterTokenRequest& request)
561 {
562     static NeverDestroyed<String> safeJavaScriptURL(MAKE_STATIC_STRING_IMPL("javascript:void(0)"));
563
564     bool didBlockScript = false;
565     for (size_t i = 0; i < request.token.attributes().size(); ++i) {
566         const HTMLToken::Attribute& attribute = request.token.attributes().at(i);
567         bool isInlineEventHandler = isNameOfInlineEventHandler(attribute.name);
568         // FIXME: It would be better if we didn't create a new String for every attribute in the document.
569         String strippedValue = stripLeadingAndTrailingHTMLSpaces(String(attribute.value));
570         bool valueContainsJavaScriptURL = (!isInlineEventHandler && WTF::protocolIsJavaScript(strippedValue)) || (isSemicolonSeparatedAttribute(attribute) && semicolonSeparatedValueContainsJavaScriptURL(strippedValue));
571         if (!isInlineEventHandler && !valueContainsJavaScriptURL)
572             continue;
573         if (!isContainedInRequest(canonicalize(snippetFromAttribute(request, attribute), TruncationStyle::ScriptLikeAttribute)))
574             continue;
575         request.token.eraseValueOfAttribute(i);
576         if (valueContainsJavaScriptURL)
577             request.token.appendToAttributeValue(i, safeJavaScriptURL.get());
578         didBlockScript = true;
579     }
580     return didBlockScript;
581 }
582
583 bool XSSAuditor::eraseAttributeIfInjected(const FilterTokenRequest& request, const QualifiedName& attributeName, const String& replacementValue, TruncationStyle truncationStyle)
584 {
585     size_t indexOfAttribute = 0;
586     if (!findAttributeWithName(request.token, attributeName, indexOfAttribute))
587         return false;
588
589     const HTMLToken::Attribute& attribute = request.token.attributes().at(indexOfAttribute);
590     if (!isContainedInRequest(canonicalize(snippetFromAttribute(request, attribute), truncationStyle)))
591         return false;
592
593     if (threadSafeMatch(attributeName, srcAttr)) {
594         if (isLikelySafeResource(String(attribute.value)))
595             return false;
596     } else if (threadSafeMatch(attributeName, http_equivAttr)) {
597         if (!isDangerousHTTPEquiv(String(attribute.value)))
598             return false;
599     }
600
601     request.token.eraseValueOfAttribute(indexOfAttribute);
602     if (!replacementValue.isEmpty())
603         request.token.appendToAttributeValue(indexOfAttribute, replacementValue);
604     return true;
605 }
606
607 String XSSAuditor::canonicalizedSnippetForTagName(const FilterTokenRequest& request)
608 {
609     // Grab a fixed number of characters equal to the length of the token's name plus one (to account for the "<").
610     return canonicalize(request.sourceTracker.source(request.token).substring(0, request.token.name().size() + 1), TruncationStyle::None);
611 }
612
613 String XSSAuditor::snippetFromAttribute(const FilterTokenRequest& request, const HTMLToken::Attribute& attribute)
614 {
615     // The range doesn't include the character which terminates the value. So,
616     // for an input of |name="value"|, the snippet is |name="value|. For an
617     // unquoted input of |name=value |, the snippet is |name=value|.
618     // FIXME: We should grab one character before the name also.
619     return request.sourceTracker.source(request.token, attribute.startOffset, attribute.endOffset);
620 }
621
622 String XSSAuditor::canonicalize(const String& snippet, TruncationStyle truncationStyle)
623 {
624     String decodedSnippet = fullyDecodeString(snippet, m_encoding);
625     if (truncationStyle != TruncationStyle::None) {
626         decodedSnippet.truncate(kMaximumFragmentLengthTarget);
627         if (truncationStyle == TruncationStyle::SrcLikeAttribute)
628             truncateForSrcLikeAttribute(decodedSnippet);
629         else if (truncationStyle == TruncationStyle::ScriptLikeAttribute)
630             truncateForScriptLikeAttribute(decodedSnippet);
631     }
632     return decodedSnippet.removeCharacters(&isNonCanonicalCharacter);
633 }
634
635 String XSSAuditor::canonicalizedSnippetForJavaScript(const FilterTokenRequest& request)
636 {
637     String string = request.sourceTracker.source(request.token);
638     size_t startPosition = 0;
639     size_t endPosition = string.length();
640     size_t foundPosition = notFound;
641     size_t lastNonSpacePosition = notFound;
642
643     // Skip over initial comments to find start of code.
644     while (startPosition < endPosition) {
645         while (startPosition < endPosition && isHTMLSpace(string[startPosition]))
646             startPosition++;
647
648         // Under SVG/XML rules, only HTML comment syntax matters and the parser returns
649         // these as a separate comment tokens. Having consumed whitespace, we need not look
650         // further for these.
651         if (request.shouldAllowCDATA)
652             break;
653
654         // Under HTML rules, both the HTML and JS comment synatx matters, and the HTML
655         // comment ends at the end of the line, not with -->.
656         if (startsHTMLCommentAt(string, startPosition) || startsSingleLineCommentAt(string, startPosition)) {
657             while (startPosition < endPosition && !isJSNewline(string[startPosition]))
658                 startPosition++;
659         } else if (startsMultiLineCommentAt(string, startPosition)) {
660             if (startPosition + 2 < endPosition && (foundPosition = string.find("*/", startPosition + 2)) != notFound)
661                 startPosition = foundPosition + 2;
662             else
663                 startPosition = endPosition;
664         } else
665             break;
666     }
667
668     String result;
669     while (startPosition < endPosition && !result.length()) {
670         // Stop at next comment (using the same rules as above for SVG/XML vs HTML), when we encounter a comma,
671         // when we hit an opening <script> tag, or when we exceed the maximum length target. The comma rule
672         // covers a common parameter concatenation case performed by some web servers.
673         lastNonSpacePosition = notFound;
674         for (foundPosition = startPosition; foundPosition < endPosition; foundPosition++) {
675             if (!request.shouldAllowCDATA) {
676                 if (startsSingleLineCommentAt(string, foundPosition)
677                     || startsMultiLineCommentAt(string, foundPosition)
678                     || startsHTMLCommentAt(string, foundPosition)) {
679                     break;
680                 }
681             }
682             if (string[foundPosition] == ',')
683                 break;
684
685             if (lastNonSpacePosition != notFound && startsOpeningScriptTagAt(string, foundPosition)) {
686                 foundPosition = lastNonSpacePosition + 1;
687                 break;
688             }
689             if (foundPosition > startPosition + kMaximumFragmentLengthTarget) {
690                 // After hitting the length target, we can only stop at a point where we know we are
691                 // not in the middle of a %-escape sequence. For the sake of simplicity, approximate
692                 // not stopping inside a (possibly multiply encoded) %-escape sequence by breaking on
693                 // whitespace only. We should have enough text in these cases to avoid false positives.
694                 if (isHTMLSpace(string[foundPosition]))
695                     break;
696             }
697
698             if (!isHTMLSpace(string[foundPosition]))
699                 lastNonSpacePosition = foundPosition;
700         }
701
702         result = canonicalize(string.substring(startPosition, foundPosition - startPosition), TruncationStyle::None);
703         startPosition = foundPosition + 1;
704     }
705     return result;
706 }
707
708 SuffixTree<ASCIICodebook>* XSSAuditor::decodedHTTPBodySuffixTree()
709 {
710     const unsigned minimumLengthForSuffixTree = 512; // FIXME: Tune this parameter.
711     const unsigned suffixTreeDepth = 5;
712
713     if (!m_decodedHTTPBodySuffixTree && m_decodedHTTPBody.length() >= minimumLengthForSuffixTree)
714         m_decodedHTTPBodySuffixTree = std::make_unique<SuffixTree<ASCIICodebook>>(m_decodedHTTPBody, suffixTreeDepth);
715     return m_decodedHTTPBodySuffixTree.get();
716 }
717
718 bool XSSAuditor::isContainedInRequest(const String& decodedSnippet)
719 {
720     if (decodedSnippet.isEmpty())
721         return false;
722     if (m_decodedURL.containsIgnoringASCIICase(decodedSnippet))
723         return true;
724     auto* decodedHTTPBodySuffixTree = this->decodedHTTPBodySuffixTree();
725     if (decodedHTTPBodySuffixTree && !decodedHTTPBodySuffixTree->mightContain(decodedSnippet))
726         return false;
727     return m_decodedHTTPBody.containsIgnoringASCIICase(decodedSnippet);
728 }
729
730 bool XSSAuditor::isLikelySafeResource(const String& url)
731 {
732     // Give empty URLs and about:blank a pass. Making a resourceURL from an
733     // empty string below will likely later fail the "no query args test" as
734     // it inherits the document's query args.
735     if (url.isEmpty() || url == WTF::blankURL().string())
736         return true;
737
738     // If the resource is loaded from the same host as the enclosing page, it's
739     // probably not an XSS attack, so we reduce false positives by allowing the
740     // request, ignoring scheme and port considerations. If the resource has a
741     // query string, we're more suspicious, however, because that's pretty rare
742     // and the attacker might be able to trick a server-side script into doing
743     // something dangerous with the query string.  
744     if (m_documentURL.host().isEmpty())
745         return false;
746
747     URL resourceURL(m_documentURL, url);
748     return (m_documentURL.host() == resourceURL.host() && resourceURL.query().isEmpty());
749 }
750
751 } // namespace WebCore