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