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