Revision: 5464
Author: [email protected]
Date: Mon Jul 1 09:53:11 2013
Log: [No log message]
http://code.google.com/p/google-caja/source/detail?r=5464
Modified:
/trunk/src/com/google/caja/plugin/sanitizecss.js
/trunk/tests/com/google/caja/plugin/css-stylesheet-tests.js
/trunk/tests/com/google/caja/plugin/sanitizecss_test.js
/trunk/third_party/js/jsunit/2.2/jsUnitCore.js
=======================================
--- /trunk/src/com/google/caja/plugin/sanitizecss.js Mon Jun 24 15:05:32
2013
+++ /trunk/src/com/google/caja/plugin/sanitizecss.js Mon Jul 1 09:53:11
2013
@@ -119,7 +119,7 @@
* style will be resolved
*/
sanitizeCssProperty = (function () {
-
+
function unionArrays(arrs) {
var map = {};
for (var i = arrs.length; --i >= 0;) {
@@ -130,7 +130,7 @@
}
return map;
}
-
+
/**
* Normalize tokens within a function call they can match against
* cssSchema[propName].cssExtra.
@@ -160,10 +160,10 @@
tokens.length = j;
return tokens.join(' ');
}
-
+
// Used as map value to avoid hasOwnProperty checks.
var ALLOWED_LITERAL = {};
-
+
return function (property, propertySchema, tokens,
opt_naiveUriRewriter, opt_baseUri) {
var propBits = propertySchema.cssPropBits;
@@ -174,7 +174,7 @@
CSS_PROP_BIT_QSTRING_CONTENT | CSS_PROP_BIT_QSTRING_URL);
// TODO(mikesamuel): Figure out what to do with props like
// content that admit both URLs and strings.
-
+
// Used to join unquoted keywords into a single quoted string.
var lastQuoted = NaN;
var i = 0, k = 0;
@@ -276,8 +276,9 @@
? (lastQuoted+1 === k
// If the last token was also a keyword that was quoted,
then
// combine this token into that.
- ? (tokens[lastQuoted] = tokens[lastQuoted]
- .substring(0, tokens[lastQuoted].length-1) + ' ' + token
+ '"',
+ ? (tokens[lastQuoted] = (
+ tokens[lastQuoted].substring(0,
tokens[lastQuoted].length-1)
+ + ' ' + token + '"'),
token = '')
: (lastQuoted = k, '"' + token + '"'))
// Disallowed.
@@ -292,7 +293,18 @@
tokens.length = k;
};
})();
-
+
+ var HISTORY_NON_SENSITIVE_PSEUDO_SELECTOR_WHITELIST =
+ /^(active|after|before|first-child|first-letter|focus|hover)$/;
+
+ // TODO: This should be removed now as modern browsers no longer require
+ // this special handling
+ var HISTORY_SENSITIVE_PSEUDO_SELECTOR_WHITELIST = /^(link|visited)$/;
+
+ // Set of punctuation tokens that are child/sibling selectors.
+ var COMBINATOR = {};
+ COMBINATOR['>'] = COMBINATOR['+'] = COMBINATOR['~'] = COMBINATOR;
+
/**
* Given a series of tokens, returns two lists of sanitized selectors.
* @param {Array.<string>} selectors In the form produces by csslexer.js.
@@ -314,31 +326,24 @@
// property groups for the history sensitive ones.
var historySensitiveSelectors = [];
var historyInsensitiveSelectors = [];
-
- var HISTORY_NON_SENSITIVE_PSEUDO_SELECTOR_WHITELIST =
- /^(active|after|before|first-child|first-letter|focus|hover)$/;
-
- // TODO: This should be removed now as modern browsers no longer
require
- // this special handling
- var HISTORY_SENSITIVE_PSEUDO_SELECTOR_WHITELIST =
- /^(link|visited)$/;
-
+
// Remove any spaces that are not operators.
var k = 0, i, inBrackets = 0, tok;
for (i = 0; i < selectors.length; ++i) {
tok = selectors[i];
-
+
if (
(tok == '(' || tok == '[') ? (++inBrackets, true)
: (tok == ')' || tok == ']') ? (inBrackets && --inBrackets, true)
: !(selectors[i] == ' '
- && (inBrackets || selectors[i-1] == '>' || selectors[i+1]
== '>'))
+ && (inBrackets || COMBINATOR[selectors[i-1]] === COMBINATOR
+ || COMBINATOR[selectors[i+1]] === COMBINATOR))
) {
selectors[k++] = selectors[i];
}
}
selectors.length = k;
-
+
// Split around commas. If there is an error in one of the comma
separated
// bits, we throw the whole away, but the failure of one selector does
not
// affect others.
@@ -350,15 +355,15 @@
}
}
processSelector(start, n);
-
-
+
+
function processSelector(start, end) {
var historySensitive = false;
-
+
// Space around commas is not an operator.
if (selectors[start] === ' ') { ++start; }
if (end-1 !== start && selectors[end] === ' ') { --end; }
-
+
// Split the selector into element selectors, content around
// space (ancestor operator) and '>' (descendant operator).
var out = [];
@@ -366,22 +371,21 @@
var elSelector = '';
for (var i = start; i < end; ++i) {
var tok = selectors[i];
- var isChild = (tok === '>');
- if (isChild || tok === ' ') {
+ if (COMBINATOR[tok] === COMBINATOR || tok === ' ') {
// We've found the end of a single link in the selector chain.
// We disallow absolute positions relative to html.
elSelector = processElementSelector(lastOperator, i, false);
- if (!elSelector || (isChild && /^html/i.test(elSelector))) {
+ if (!elSelector || (tok === '>' && /^html/i.test(elSelector))) {
return;
}
lastOperator = i+1;
- out.push(elSelector, isChild ? ' > ' : ' ');
+ out.push(elSelector, tok);
}
}
elSelector = processElementSelector(lastOperator, end, true);
if (!elSelector) { return; }
out.push(elSelector);
-
+
function processElementSelector(start, end, last) {
// Split the element selector into four parts.
// DIV.foo#bar[href]:hover
@@ -406,7 +410,9 @@
}
}
classId = '';
- while (start < end) {
+ attrs = '';
+ pseudoSelector = '';
+ for (;start < end; ++start) {
tok = selectors[start];
if (tok.charAt(0) === '#') {
if (/^#_|__$|[^#0-9A-Za-z:_\-]/.test(tok)) { return null; }
@@ -420,57 +426,92 @@
} else {
return null;
}
- } else {
- break;
- }
- ++start;
- }
- attrs = '';
- while (start < end && selectors[start] === '[') {
- ++start;
- var attr = selectors[start++];
- var atype = html4.ATTRIBS[element + '::' + attr];
- if (atype !== +atype) { atype = html4.ATTRIBS['*::' + attr]; }
- if (atype !== +atype) { return null; }
-
- var op = '', value = '';
- if (/^[~^$*|]?=$/.test(selectors[start])) {
- op = selectors[start++];
- value = selectors[start++];
- }
- if (selectors[start++] !== ']') { return null; }
- // TODO: replace this with a lookup table that also provides a
- // function from operator and value to testable value.
- switch (atype) {
+ } else if (start < end && selectors[start] === '[') {
+ ++start;
+ var attr = selectors[start++].toLowerCase();
+ var atype = html4.ATTRIBS[element + '::' + attr];
+ if (atype !== +atype) { atype = html4.ATTRIBS['*::' + attr]; }
+ if (atype !== +atype) { return null; }
+
+ var op = '', value = '', ignoreCase = false;
+ if (/^[~^$*|]?=$/.test(selectors[start])) {
+ op = selectors[start++];
+ value = selectors[start++];
+ // Quote identifier values.
+ if (/^[0-9A-Za-z:_\-]+$/.test(value)) {
+ value = '"' + value + '"';
+ } else if (value === ']') {
+ value = '""';
+ --start;
+ }
+ // Reject unquoted values.
+ if (!/^"([^\"\\]|\\.)*"$/.test(value)) {
+ return null;
+ }
+ ignoreCase = selectors[start] === "i";
+ if (ignoreCase) { ++start; }
+ }
+ if (selectors[start] !== ']') {
+ ++start;
+ return null;
+ }
+ // TODO: replace this with a lookup table that also provides a
+ // function from operator and value to testable value.
+ switch (atype) {
+ case html4.atype['CLASSES']:
+ case html4.atype['LOCAL_NAME']:
case html4.atype['NONE']:
- case html4.atype['URI']:
- case html4.atype['URI_FRAGMENT']:
+ break;
+ case html4.atype['GLOBAL_NAME']:
case html4.atype['ID']:
case html4.atype['IDREF']:
- case html4.atype['IDREFS']:
- case html4.atype['GLOBAL_NAME']:
- case html4.atype['LOCAL_NAME']:
- case html4.atype['CLASSES']:
- if (op && atype !== html4.atype['NONE']) { return null; }
- attrs += '[' + attr + op + value + ']';
+ if ((op === '=' || op === '~=' || op === '$=')
+ && value != '""' && !ignoreCase) {
+ // The suffix is case-sensitive, so we can't translate case
+ // ignoring matches.
+ value = '"'
+ + value.substring(1, value.length-1) + "-" + suffix
+ + '"';
+ } else if (op === '|=' || op === '') {
+ // Ok. a|=b -> a == b || a.startsWith(b + "-") and since
we
+ // use "-" to separate the suffix from the identifier, we
can
+ // allow this through unmodified.
+ // Existence checks are also ok.
+ } else {
+ // Can't correctly handle prefix and substring operators
+ // without leaking information about the suffix.
+ op = null;
+ }
break;
- }
- }
- pseudoSelector = '';
- if (start < end && selectors[start] === ':') {
- tok = selectors[++start];
- if (HISTORY_SENSITIVE_PSEUDO_SELECTOR_WHITELIST.test(tok)) {
- if (!/^[a*]?$/.test(element)) {
- return null;
+ case html4.atype['URI']:
+ case html4.atype['URI_FRAGMENT']:
+ // URIs are rewritten, so we can't meanginfully translate URI
+ // selectors besides the common a[href] one that is used to
+ // distinguish links from naming anchors.
+ if (op !== '') { return null; }
+ break;
+ // TODO: IDREFS
+ default:
+ op = null;
}
- historySensitive = true;
- pseudoSelector = ':' + tok;
- ++start;
- element = 'a';
- } else if
(HISTORY_NON_SENSITIVE_PSEUDO_SELECTOR_WHITELIST.test(tok)) {
- historySensitive = false;
- pseudoSelector = ':' + tok;
- ++start;
+ if (op == null) { return null; }
+ attrs += '[' + attr + op + value + (ignoreCase ? ' i]' : ']');
+ } else if (start < end && selectors[start] === ':') {
+ tok = selectors[++start];
+ if (HISTORY_SENSITIVE_PSEUDO_SELECTOR_WHITELIST.test(tok)) {
+ if (!/^[a*]?$/.test(element)) {
+ return null;
+ }
+ historySensitive = true;
+ pseudoSelector = ':' + tok;
+ element = 'a';
+ } else if (
+ HISTORY_NON_SENSITIVE_PSEUDO_SELECTOR_WHITELIST.test(tok)) {
+ historySensitive = false;
+ pseudoSelector = ':' + tok;
+ } else {
+ break;
+ }
}
}
if (start === end) {
@@ -482,21 +523,21 @@
}
return null;
}
-
-
+
+
var safeSelector = out.join('');
// Namespace the selector so that it only matches under
// a node with suffix in its CLASS attribute.
safeSelector = '.' + suffix + ' ' + safeSelector;
-
+
(historySensitive
? historySensitiveSelectors
: historyInsensitiveSelectors).push(safeSelector);
}
-
+
return [historyInsensitiveSelectors, historySensitiveSelectors];
};
-
+
(function () {
var allowed = {};
var cssMediaTypeWhitelist = {
@@ -510,7 +551,7 @@
'tty': allowed,
'tv': allowed
};
-
+
/**
* Given a series of sanitized tokens, removes any properties that
would
* leak user history if allowed to style links differently depending on
@@ -522,7 +563,7 @@
for (var i = 0, n = blockOfProperties.length; i < n-1; ++i) {
var token = blockOfProperties[i];
if (':' === blockOfProperties[i+1]) {
- elide =
+ elide =
!(cssSchema[token].cssPropBits & CSS_PROP_BIT_ALLOWED_IN_LINK);
}
if (elide) { blockOfProperties[i] = ''; }
@@ -530,7 +571,7 @@
}
return blockOfProperties.join('');
}
-
+
/**
* Extracts a url out of an at-import rule of the form:
* \@import "mystyle.css";
@@ -560,7 +601,7 @@
}
return null;
}
-
+
/**
* @param {string} baseUri a string against which relative urls are
* resolved.
@@ -622,7 +663,7 @@
resolveUri(baseUri, cssParseUri(headerArray[0])),
function(result) {
var sanitized =
- sanitizeStylesheetInternal(cssUrl, result.html,
+ sanitizeStylesheetInternal(cssUrl, result.html,
suffix,
naiveUriRewriter, naiveUriFetcher, tagPolicy,
continuation);
@@ -752,13 +793,13 @@
moreToCome : moreToCome
};
}
-
+
sanitizeStylesheet = function (
baseUri, cssText, suffix, naiveUriRewriter, tagPolicy) {
return sanitizeStylesheetInternal(baseUri, cssText, suffix,
naiveUriRewriter, undefined, tagPolicy, undefined).result;
};
-
+
sanitizeStylesheetWithExternals = function (baseUri, cssText, suffix,
naiveUriRewriter, naiveUriFetcher, tagPolicy,
continuation) {
=======================================
--- /trunk/tests/com/google/caja/plugin/css-stylesheet-tests.js Mon Apr 8
12:16:59 2013
+++ /trunk/tests/com/google/caja/plugin/css-stylesheet-tests.js Mon Jul 1
09:53:11 2013
@@ -206,11 +206,11 @@
},
{
"cssText": "* html > * > p { margin: 0; }",
- "golden": ".namespace__ * caja-v-html > * > p{margin:0}"
+ "golden": ".namespace__ * caja-v-html>*>p{margin:0}"
},
{
"cssText": "#foo > #bar { color: blue }",
- "golden": ".namespace__ #foo-namespace__ >
#bar-namespace__{color:blue}"
+ "golden": ".namespace__
#foo-namespace__>#bar-namespace__{color:blue}"
},
{
"cssText": "#foo .bar { color: blue }",
=======================================
--- /trunk/tests/com/google/caja/plugin/sanitizecss_test.js Mon Jun 24
15:05:32 2013
+++ /trunk/tests/com/google/caja/plugin/sanitizecss_test.js Mon Jul 1
09:53:11 2013
@@ -227,32 +227,73 @@
var sanitized = sanitizeCssSelectors(
tokens, 'sfx', function(el, args) { return { tagName: el }; });
assertArrayEquals(
- [['.sfx input.cl\\:a\\:ss[type="text"]',
- '.sfx input#foo\\:bar-sfx'], []],
+ [['.sfx input.cl\\:a\\:ss[type="text"]',
+ '.sfx input#foo\\:bar-sfx'], []],
sanitized);
jsunit.pass();
});
+function assertSelector(source, prefix, expected) {
+ var tokens = lexCss(source);
+ var sanitized = sanitizeCssSelectors(
+ tokens, prefix, function(el, args) { return { tagName: el }; });
+ assertArrayEquals(expected, sanitized);
+}
+
jsunitRegister('testCssSelectors',
function testCssSelectors() {
- function assertSelector(source, prefix, expected) {
- var tokens = lexCss(source);
- var sanitized = sanitizeCssSelectors(
- tokens, prefix, function(el, args) { return { tagName: el }; });
- assertArrayEquals(expected, sanitized);
- }
+ assertSelector("#foo:visited", "sfx", [[], [".sfx a#foo-sfx:visited"]]);
+ assertSelector("#foo:link", "sfx", [[], [".sfx a#foo-sfx:link"]]);
- assertSelector("#foo:visited", "sfx", [[], ".sfx #foo-sfx:visited"]);
- assertSelector("#foo:link", "sfx", [[], ".sfx #foo-sfx:link"]);
+ assertSelector("#foo:active", "sfx", [[".sfx #foo-sfx:active"], []]);
+ assertSelector("#foo:after", "sfx", [[".sfx #foo-sfx:after"], []]);
+ assertSelector("#foo:before", "sfx", [[".sfx #foo-sfx:before"], []]);
+ assertSelector(
+ "#foo:first-child", "sfx", [[".sfx #foo-sfx:first-child"], []]);
+ assertSelector(
+ "#foo:first-letter", "sfx", [[".sfx #foo-sfx:first-letter"], []]);
+ assertSelector("#foo:focus", "sfx", [[".sfx #foo-sfx:focus"], []]);
+ assertSelector("#foo:hover", "sfx", [[".sfx #foo-sfx:hover"], []]);
+ assertSelector("#foo:bogus", "sfx", [[], []]);
+ jsunit.pass();
+});
- assertSelector("#foo:active", "sfx", [".sfx #foo-sfx:active", []]);
- assertSelector("#foo:after", "sfx", [".sfx #foo-sfx:after", []]);
- assertSelector("#foo:before", "sfx", [".sfx #foo-sfx:before", []]);
- assertSelector("#foo:first-child", "sfx", [".sfx #foo-sfx:first-child",
[]]);
- assertSelector("#foo:first-letter", "sfx", [".sfx #foo-sfx:first-leter",
[]]);
- assertSelector("#foo:focus", "sfx", [".sfx #foo-sfx:focus", []]);
- assertSelector("#foo:hover", "sfx", [".sfx #foo-sfx:hover", []]);
- assertSelector("#foo:bogus", "sfx", [".sfx #foo-sfx", []]);
+jsunitRegister('testAttrSelectors',
+ function testAttrSelectors() {
+ assertSelector(
+ "div[class*='substr']", "sfx", [[".sfx div[class*=\"substr\"]"], []]);
+ assertSelector(
+ "div[class|='substr' i]", "sfx", [[".sfx div[class|=\"substr\" i]"],
[]]);
+ assertSelector(
+ "p[title |= \"sub\"]", "sfx", [[".sfx p[title|=\"sub\"]"], []]);
+ assertSelector(
+ "p[id ~= \"\\\"\"]", "sfx", [[".sfx p[id~=\"\\22 -sfx\"]"], []]);
+ // ids allowed on any element. unquoted values are quoted.
+ assertSelector("*[id ~= foo]", "sfx", [[".sfx *[id~=\"foo-sfx\"]"], []]);
+ // id existence check allowed
+ assertSelector("*[id]", "sfx", [[".sfx *[id]"], []]);
+ assertSelector(
+ "input[type=text]", "sfx", [[".sfx input[type=\"text\"]"], []]);
+ // Can't deal with case insensitive matches of case sensitive suffix.
+ assertSelector("p[id ~= \"\\\"\" i]", "sfx", [[], []]);
+ // Drop empty values for suffix and prefix operators instead of turning a
+ // predicate that always fails into one that can succeed.
+ assertSelector("p[id^='']", "sfx", [[], []]);
+ // URIs require rewriting, so it isn't meaningful to match against URIs.
+ // Also, it leaks the base URL.
+ // Maybe store the original of URI attrs in a custom attr.
+ assertSelector("a[href*='?pwd=hello-kitty']", "sfx", [[], []]);
+ assertSelector("a[href]", "sfx", [[".sfx a[href]"], []]);
+ assertSelector("A[href]", "sfx", [[".sfx a[href]"], []]);
+ assertSelector("A[HREF]", "sfx", [[".sfx a[href]"], []]);
+ jsunit.pass();
+});
+
+jsunitRegister('testMixedSubselectorsOrderIndependent',
+ function testMixedSubselectorsOrderIndependent() {
+ assertSelector(
+ "div[title=foo].c1#id.c2", "zzz",
+ [[".zzz div.c1#id-zzz.c2[title=\"foo\"]"], []]);
jsunit.pass();
});
=======================================
--- /trunk/third_party/js/jsunit/2.2/jsUnitCore.js Mon May 6 13:38:03 2013
+++ /trunk/third_party/js/jsunit/2.2/jsUnitCore.js Mon Jul 1 09:53:11 2013
@@ -301,16 +301,22 @@
isEqual = (var1.toString() === var2.toString());
break;
default: //Object | Array
+ isEqual = var1.constructor === var2.constructor;
var i;
- if (isEqual = (var1.length === var2.length))
- for (i in var1)
- assertObjectEquals(msg + ' found nested ' + type
+ '@' + i + '\n', var1[i], var2[i]);
+ for (i in var1) {
+ assertObjectEquals(msg + ' found nested ' + type + '@'
+ i + '\n', var1[i], var2[i]);
+ }
+ for (i in var2) {
+ if (!(i in var1)) {
+ isEqual = false;
+ }
+ }
}
- var ds1 = _displayStringForValue(var1);
- var ds2 = _displayStringForValue(var2);
- var sep = Math.max(ds1.length, ds2.length) < 40 ? ' ' : '\n';
- _assert(msg, isEqual, 'Expected' + sep + ds1 + sep + 'but was' +
sep + ds2);
}
+ var ds1 = _displayStringForValue(var1);
+ var ds2 = _displayStringForValue(var2);
+ var sep = Math.max(ds1.length, ds2.length) < 40 ? ' ' : '\n';
+ _assert(msg, isEqual, 'Expected' + sep + ds1 + sep + 'but was' + sep +
ds2);
}
assertArrayEquals = assertObjectEquals;
--
---
You received this message because you are subscribed to the Google Groups "Google Caja Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
For more options, visit https://groups.google.com/groups/opt_out.