#include "config.h"
#include "XSSAuditor.h"
#include "Console.h"
#include "DOMWindow.h"
#include "DecodeEscapeSequences.h"
#include "Document.h"
#include "DocumentLoader.h"
#include "Frame.h"
#include "FrameLoaderClient.h"
#include "HTMLDocumentParser.h"
#include "HTMLNames.h"
#include "HTMLTokenizer.h"
#include "HTMLParamElement.h"
#include "HTMLParserIdioms.h"
#include "SecurityOrigin.h"
#include "Settings.h"
#include "TextEncoding.h"
#include "TextResourceDecoder.h"
#include <wtf/text/CString.h>
namespace WebCore {
using namespace HTMLNames;
static bool isNonCanonicalCharacter(UChar c)
{
return (c == '\\' || c == '0' || c == '\0' || c >= 127);
}
static String canonicalize(const String& string)
{
return string.removeCharacters(&isNonCanonicalCharacter);
}
static bool isRequiredForInjection(UChar c)
{
return (c == '\'' || c == '"' || c == '<' || c == '>');
}
static bool isTerminatingCharacter(UChar c)
{
return (c == '&' || c == '/' || c == '"' || c == '\'' || c == '<');
}
static bool isHTMLQuote(UChar c)
{
return (c == '"' || c == '\'');
}
static bool isJSNewline(UChar c)
{
return (c == '\n' || c == '\r' || c == 0x2028 || c == 0x2029);
}
static bool startsHTMLCommentAt(const String& string, size_t start)
{
return (start + 3 < string.length() && string[start] == '<' && string[start+1] == '!' && string[start+2] == '-' && string[start+3] == '-');
}
static bool startsSingleLineCommentAt(const String& string, size_t start)
{
return (start + 1 < string.length() && string[start] == '/' && string[start+1] == '/');
}
static bool startsMultiLineCommentAt(const String& string, size_t start)
{
return (start + 1 < string.length() && string[start] == '/' && string[start+1] == '*');
}
static bool hasName(const HTMLToken& token, const QualifiedName& name)
{
return equalIgnoringNullity(token.name(), static_cast<const String&>(name.localName()));
}
static bool findAttributeWithName(const HTMLToken& token, const QualifiedName& name, size_t& indexOfMatchingAttribute)
{
for (size_t i = 0; i < token.attributes().size(); ++i) {
if (equalIgnoringNullity(token.attributes().at(i).m_name, name.localName())) {
indexOfMatchingAttribute = i;
return true;
}
}
return false;
}
static bool isNameOfInlineEventHandler(const Vector<UChar, 32>& name)
{
const size_t lengthOfShortestInlineEventHandlerName = 5; if (name.size() < lengthOfShortestInlineEventHandlerName)
return false;
return name[0] == 'o' && name[1] == 'n';
}
static bool isDangerousHTTPEquiv(const String& value)
{
String equiv = value.stripWhiteSpace();
return equalIgnoringCase(equiv, "refresh") || equalIgnoringCase(equiv, "set-cookie");
}
static inline String decode16BitUnicodeEscapeSequences(const String& string)
{
return decodeEscapeSequences<Unicode16BitEscapeSequence>(string, UTF8Encoding());
}
static inline String decodeStandardURLEscapeSequences(const String& string, const TextEncoding& encoding)
{
return decodeEscapeSequences<URLEscapeSequence>(string, encoding);
}
static String fullyDecodeString(const String& string, const TextResourceDecoder* decoder)
{
const TextEncoding& encoding = decoder ? decoder->encoding() : UTF8Encoding();
size_t oldWorkingStringLength;
String workingString = string;
do {
oldWorkingStringLength = workingString.length();
workingString = decode16BitUnicodeEscapeSequences(decodeStandardURLEscapeSequences(workingString, encoding));
} while (workingString.length() < oldWorkingStringLength);
workingString.replace('+', ' ');
workingString = canonicalize(workingString);
return workingString;
}
XSSAuditor::XSSAuditor(HTMLDocumentParser* parser)
: m_parser(parser)
, m_isEnabled(false)
, m_xssProtection(XSSProtectionEnabled)
, m_state(Uninitialized)
, m_shouldAllowCDATA(false)
, m_scriptTagNestingLevel(0)
, m_notifiedClient(false)
{
ASSERT(m_parser);
if (Frame* frame = parser->document()->frame()) {
if (Settings* settings = frame->settings())
m_isEnabled = settings->xssAuditorEnabled();
}
}
void XSSAuditor::init()
{
const size_t miniumLengthForSuffixTree = 512; const int suffixTreeDepth = 5;
ASSERT(m_state == Uninitialized);
m_state = Initialized;
if (!m_isEnabled)
return;
if (!m_parser->document()->frame()) {
m_isEnabled = false;
return;
}
const KURL& url = m_parser->document()->url();
if (url.isEmpty()) {
m_isEnabled = false;
return;
}
if (url.protocolIsData()) {
m_isEnabled = false;
return;
}
TextResourceDecoder* decoder = m_parser->document()->decoder();
m_decodedURL = fullyDecodeString(url.string(), decoder);
if (m_decodedURL.find(isRequiredForInjection) == notFound)
m_decodedURL = String();
if (DocumentLoader* documentLoader = m_parser->document()->frame()->loader()->documentLoader()) {
DEFINE_STATIC_LOCAL(String, XSSProtectionHeader, ("X-XSS-Protection"));
m_xssProtection = parseXSSProtectionHeader(documentLoader->response().httpHeaderField(XSSProtectionHeader));
FormData* httpBody = documentLoader->originalRequest().httpBody();
if (httpBody && !httpBody->isEmpty()) {
String httpBodyAsString = httpBody->flattenToString();
if (!httpBodyAsString.isEmpty()) {
m_decodedHTTPBody = fullyDecodeString(httpBodyAsString, decoder);
if (m_decodedHTTPBody.find(isRequiredForInjection) == notFound)
m_decodedHTTPBody = String();
if (m_decodedHTTPBody.length() >= miniumLengthForSuffixTree)
m_decodedHTTPBodySuffixTree = adoptPtr(new SuffixTree<ASCIICodebook>(m_decodedHTTPBody, suffixTreeDepth));
}
}
}
if (m_decodedURL.isEmpty() && m_decodedHTTPBody.isEmpty())
m_isEnabled = false;
}
void XSSAuditor::filterToken(HTMLToken& token)
{
if (m_state == Uninitialized)
init();
ASSERT(m_state == Initialized);
if (!m_isEnabled || m_xssProtection == XSSProtectionDisabled)
return;
bool didBlockScript = false;
if (token.type() == HTMLTokenTypes::StartTag)
didBlockScript = filterStartToken(token);
else if (m_scriptTagNestingLevel) {
if (token.type() == HTMLTokenTypes::Character)
didBlockScript = filterCharacterToken(token);
else if (token.type() == HTMLTokenTypes::EndTag)
filterEndToken(token);
}
if (didBlockScript) {
DEFINE_STATIC_LOCAL(String, consoleMessage, ("Refused to execute a JavaScript script. Source code of script found within request.\n"));
m_parser->document()->addConsoleMessage(JSMessageSource, LogMessageType, ErrorMessageLevel, consoleMessage);
bool didBlockEntirePage = (m_xssProtection == XSSProtectionBlockEnabled);
if (didBlockEntirePage)
m_parser->document()->frame()->loader()->stopAllLoaders();
if (!m_notifiedClient) {
m_parser->document()->frame()->loader()->client()->didDetectXSS(m_parser->document()->url(), didBlockEntirePage);
m_notifiedClient = true;
}
if (didBlockEntirePage)
m_parser->document()->frame()->navigationScheduler()->scheduleLocationChange(m_parser->document()->securityOrigin(), blankURL(), String());
}
}
bool XSSAuditor::filterStartToken(HTMLToken& token)
{
bool didBlockScript = eraseDangerousAttributesIfInjected(token);
if (hasName(token, scriptTag)) {
didBlockScript |= filterScriptToken(token);
ASSERT(m_shouldAllowCDATA || !m_scriptTagNestingLevel);
m_scriptTagNestingLevel++;
} else if (hasName(token, objectTag))
didBlockScript |= filterObjectToken(token);
else if (hasName(token, paramTag))
didBlockScript |= filterParamToken(token);
else if (hasName(token, embedTag))
didBlockScript |= filterEmbedToken(token);
else if (hasName(token, appletTag))
didBlockScript |= filterAppletToken(token);
else if (hasName(token, iframeTag))
didBlockScript |= filterIframeToken(token);
else if (hasName(token, metaTag))
didBlockScript |= filterMetaToken(token);
else if (hasName(token, baseTag))
didBlockScript |= filterBaseToken(token);
else if (hasName(token, formTag))
didBlockScript |= filterFormToken(token);
return didBlockScript;
}
void XSSAuditor::filterEndToken(HTMLToken& token)
{
ASSERT(m_scriptTagNestingLevel);
if (hasName(token, scriptTag)) {
m_scriptTagNestingLevel--;
ASSERT(m_shouldAllowCDATA || !m_scriptTagNestingLevel);
}
}
bool XSSAuditor::filterCharacterToken(HTMLToken& token)
{
ASSERT(m_scriptTagNestingLevel);
if (isContainedInRequest(m_cachedDecodedSnippet) && isContainedInRequest(decodedSnippetForJavaScript(token))) {
token.eraseCharacters();
token.appendToCharacter(' '); return true;
}
return false;
}
bool XSSAuditor::filterScriptToken(HTMLToken& token)
{
ASSERT(token.type() == HTMLTokenTypes::StartTag);
ASSERT(hasName(token, scriptTag));
m_cachedDecodedSnippet = decodedSnippetForName(token);
m_shouldAllowCDATA = m_parser->tokenizer()->shouldAllowCDATA();
if (isContainedInRequest(decodedSnippetForName(token)))
return eraseAttributeIfInjected(token, srcAttr, blankURL().string(), SrcLikeAttribute);
return false;
}
bool XSSAuditor::filterObjectToken(HTMLToken& token)
{
ASSERT(token.type() == HTMLTokenTypes::StartTag);
ASSERT(hasName(token, objectTag));
bool didBlockScript = false;
if (isContainedInRequest(decodedSnippetForName(token))) {
didBlockScript |= eraseAttributeIfInjected(token, dataAttr, blankURL().string(), SrcLikeAttribute);
didBlockScript |= eraseAttributeIfInjected(token, typeAttr);
didBlockScript |= eraseAttributeIfInjected(token, classidAttr);
}
return didBlockScript;
}
bool XSSAuditor::filterParamToken(HTMLToken& token)
{
ASSERT(token.type() == HTMLTokenTypes::StartTag);
ASSERT(hasName(token, paramTag));
size_t indexOfNameAttribute;
if (!findAttributeWithName(token, nameAttr, indexOfNameAttribute))
return false;
const HTMLToken::Attribute& nameAttribute = token.attributes().at(indexOfNameAttribute);
String name = String(nameAttribute.m_value.data(), nameAttribute.m_value.size());
if (!HTMLParamElement::isURLParameter(name))
return false;
return eraseAttributeIfInjected(token, valueAttr, blankURL().string(), SrcLikeAttribute);
}
bool XSSAuditor::filterEmbedToken(HTMLToken& token)
{
ASSERT(token.type() == HTMLTokenTypes::StartTag);
ASSERT(hasName(token, embedTag));
bool didBlockScript = false;
if (isContainedInRequest(decodedSnippetForName(token))) {
didBlockScript |= eraseAttributeIfInjected(token, codeAttr, String(), SrcLikeAttribute);
didBlockScript |= eraseAttributeIfInjected(token, srcAttr, blankURL().string(), SrcLikeAttribute);
didBlockScript |= eraseAttributeIfInjected(token, typeAttr);
}
return didBlockScript;
}
bool XSSAuditor::filterAppletToken(HTMLToken& token)
{
ASSERT(token.type() == HTMLTokenTypes::StartTag);
ASSERT(hasName(token, appletTag));
bool didBlockScript = false;
if (isContainedInRequest(decodedSnippetForName(token))) {
didBlockScript |= eraseAttributeIfInjected(token, codeAttr, String(), SrcLikeAttribute);
didBlockScript |= eraseAttributeIfInjected(token, objectAttr);
}
return didBlockScript;
}
bool XSSAuditor::filterIframeToken(HTMLToken& token)
{
ASSERT(token.type() == HTMLTokenTypes::StartTag);
ASSERT(hasName(token, iframeTag));
bool didBlockScript = false;
if (isContainedInRequest(decodedSnippetForName(token))) {
didBlockScript |= eraseAttributeIfInjected(token, srcAttr, String(), SrcLikeAttribute);
didBlockScript |= eraseAttributeIfInjected(token, srcdocAttr, String(), ScriptLikeAttribute);
}
return didBlockScript;
}
bool XSSAuditor::filterMetaToken(HTMLToken& token)
{
ASSERT(token.type() == HTMLTokenTypes::StartTag);
ASSERT(hasName(token, metaTag));
return eraseAttributeIfInjected(token, http_equivAttr);
}
bool XSSAuditor::filterBaseToken(HTMLToken& token)
{
ASSERT(token.type() == HTMLTokenTypes::StartTag);
ASSERT(hasName(token, baseTag));
return eraseAttributeIfInjected(token, hrefAttr);
}
bool XSSAuditor::filterFormToken(HTMLToken& token)
{
ASSERT(token.type() == HTMLTokenTypes::StartTag);
ASSERT(hasName(token, formTag));
return eraseAttributeIfInjected(token, actionAttr);
}
bool XSSAuditor::eraseDangerousAttributesIfInjected(HTMLToken& token)
{
DEFINE_STATIC_LOCAL(String, safeJavaScriptURL, ("javascript:void(0)"));
bool didBlockScript = false;
for (size_t i = 0; i < token.attributes().size(); ++i) {
const HTMLToken::Attribute& attribute = token.attributes().at(i);
bool isInlineEventHandler = isNameOfInlineEventHandler(attribute.m_name);
bool valueContainsJavaScriptURL = !isInlineEventHandler && protocolIsJavaScript(stripLeadingAndTrailingHTMLSpaces(String(attribute.m_value.data(), attribute.m_value.size())));
if (!isInlineEventHandler && !valueContainsJavaScriptURL)
continue;
if (!isContainedInRequest(decodedSnippetForAttribute(token, attribute, ScriptLikeAttribute)))
continue;
token.eraseValueOfAttribute(i);
if (valueContainsJavaScriptURL)
token.appendToAttributeValue(i, safeJavaScriptURL);
didBlockScript = true;
}
return didBlockScript;
}
bool XSSAuditor::eraseAttributeIfInjected(HTMLToken& token, const QualifiedName& attributeName, const String& replacementValue, AttributeKind treatment)
{
size_t indexOfAttribute = 0;
if (findAttributeWithName(token, attributeName, indexOfAttribute)) {
const HTMLToken::Attribute& attribute = token.attributes().at(indexOfAttribute);
if (isContainedInRequest(decodedSnippetForAttribute(token, attribute, treatment))) {
if (attributeName == srcAttr && isSameOriginResource(String(attribute.m_value.data(), attribute.m_value.size())))
return false;
if (attributeName == http_equivAttr && !isDangerousHTTPEquiv(String(attribute.m_value.data(), attribute.m_value.size())))
return false;
token.eraseValueOfAttribute(indexOfAttribute);
if (!replacementValue.isEmpty())
token.appendToAttributeValue(indexOfAttribute, replacementValue);
return true;
}
}
return false;
}
String XSSAuditor::decodedSnippetForName(const HTMLToken& token)
{
return fullyDecodeString(m_parser->sourceForToken(token), m_parser->document()->decoder()).substring(0, token.name().size() + 1);
}
String XSSAuditor::decodedSnippetForAttribute(const HTMLToken& token, const HTMLToken::Attribute& attribute, AttributeKind treatment)
{
int start = attribute.m_nameRange.m_start - token.startIndex();
int end = attribute.m_valueRange.m_end - token.startIndex();
String decodedSnippet = fullyDecodeString(m_parser->sourceForToken(token).substring(start, end - start), m_parser->document()->decoder());
decodedSnippet.truncate(kMaximumFragmentLengthTarget);
if (treatment == SrcLikeAttribute) {
int slashCount = 0;
bool commaSeen = false;
for (size_t currentLength = 0; currentLength < decodedSnippet.length(); ++currentLength) {
UChar currentChar = decodedSnippet[currentLength];
if (currentChar == '?' || currentChar == '#' || ((currentChar == '/' || currentChar == '\\') && (commaSeen || ++slashCount > 2))) {
decodedSnippet.truncate(currentLength);
break;
}
if (currentChar == ',')
commaSeen = true;
}
} else if (treatment == ScriptLikeAttribute) {
size_t position = 0;
if ((position = decodedSnippet.find("=")) != notFound
&& (position = decodedSnippet.find(isNotHTMLSpace, position + 1)) != notFound
&& (position = decodedSnippet.find(isTerminatingCharacter, isHTMLQuote(decodedSnippet[position]) ? position + 1 : position)) != notFound) {
decodedSnippet.truncate(position);
}
}
return decodedSnippet;
}
String XSSAuditor::decodedSnippetForJavaScript(const HTMLToken& token)
{
String string = m_parser->sourceForToken(token);
size_t startPosition = 0;
size_t endPosition = string.length();
size_t foundPosition = notFound;
while (startPosition < endPosition) {
while (startPosition < endPosition && isHTMLSpace(string[startPosition]))
startPosition++;
if (m_shouldAllowCDATA)
break;
if (startsHTMLCommentAt(string, startPosition) || startsSingleLineCommentAt(string, startPosition)) {
while (startPosition < endPosition && !isJSNewline(string[startPosition]))
startPosition++;
} else if (startsMultiLineCommentAt(string, startPosition)) {
if ((foundPosition = string.find("*/", startPosition)) != notFound)
startPosition = foundPosition + 2;
else
startPosition = endPosition;
} else
break;
}
for (foundPosition = startPosition; foundPosition < endPosition; foundPosition++) {
if (!m_shouldAllowCDATA) {
if (startsSingleLineCommentAt(string, foundPosition) || startsMultiLineCommentAt(string, foundPosition)) {
endPosition = foundPosition + 2;
break;
}
if (startsHTMLCommentAt(string, foundPosition)) {
endPosition = foundPosition + 4;
break;
}
}
if (string[foundPosition] == ',' || (foundPosition > startPosition + kMaximumFragmentLengthTarget && isHTMLSpace(string[foundPosition]))) {
endPosition = foundPosition;
break;
}
}
return fullyDecodeString(string.substring(startPosition, endPosition - startPosition), m_parser->document()->decoder());
}
bool XSSAuditor::isContainedInRequest(const String& decodedSnippet)
{
if (decodedSnippet.isEmpty())
return false;
if (m_decodedURL.find(decodedSnippet, 0, false) != notFound)
return true;
if (m_decodedHTTPBodySuffixTree && !m_decodedHTTPBodySuffixTree->mightContain(decodedSnippet))
return false;
return m_decodedHTTPBody.find(decodedSnippet, 0, false) != notFound;
}
bool XSSAuditor::isSameOriginResource(const String& url)
{
KURL resourceURL(m_parser->document()->url(), url);
return (m_parser->document()->url().host() == resourceURL.host() && resourceURL.query().isEmpty());
}
}