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