-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Detect static securecookies to trivialize more rules #16029
Changes from 6 commits
77f08cf
ca2844f
4bbd731
ca52ba2
5c149aa
a37df21
db2b8b6
e8f27ee
6e82204
e5ad3c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,13 +9,14 @@ const readFile = _.wrapCallback(fs.readFile); | |
const writeFile = _.wrapCallback(fs.writeFile); | ||
const parseXML = _.wrapCallback(require('xml2js').parseString); | ||
const { explodeRegExp, UnsupportedRegExp } = require('./explode-regexp'); | ||
const escapeStringRegexp = require('escape-string-regexp'); | ||
const chalk = require('chalk'); | ||
|
||
const rulesDir = `${__dirname}/../../src/chrome/content/rules`; | ||
|
||
const tagsRegExps = new Map(); | ||
|
||
function createTagsRegexp(tag) { | ||
function createTagsRegexp (tag) { | ||
let re = tagsRegExps.get(tag); | ||
if (!re) { | ||
const tagRe = `<${tag}(?:\\s+\\w+=".*?")*\\s*\\/>`; | ||
|
@@ -25,7 +26,7 @@ function createTagsRegexp(tag) { | |
return re; | ||
} | ||
|
||
function replaceXML(source, tag, newXML) { | ||
function replaceXML (source, tag, newXML) { | ||
let pos, indent; | ||
let re = createTagsRegexp(tag); | ||
|
||
|
@@ -74,12 +75,12 @@ const rules = | |
} | ||
}); | ||
|
||
function isTrivial(rule) { | ||
function isTrivial (rule) { | ||
return rule.from === '^http:' && rule.to === 'https:'; | ||
} | ||
|
||
files.fork().zipAll([ sources.fork(), rules ]).map(([name, source, ruleset]) => { | ||
function createTag(tagName, colour, print) { | ||
function createTag (tagName, colour, print) { | ||
return (strings, ...values) => { | ||
let result = `[${tagName}] ${chalk.bold(name)}: ${strings[0]}`; | ||
for (let i = 1; i < strings.length; i++) { | ||
|
@@ -99,6 +100,7 @@ files.fork().zipAll([ sources.fork(), rules ]).map(([name, source, ruleset]) => | |
const fail = createTag('FAIL', chalk.red, console.error); | ||
|
||
let targets = ruleset.target.map(target => target.$.host); | ||
let securecookies = ruleset.securecookie ? ruleset.securecookie.map(sc => sc.$) : new Array(); | ||
let rules = ruleset.rule.map(rule => rule.$); | ||
|
||
if (rules.length === 1 && isTrivial(rules[0])) { | ||
|
@@ -108,7 +110,7 @@ files.fork().zipAll([ sources.fork(), rules ]).map(([name, source, ruleset]) => | |
let targetRe = new RegExp(`^(?:${targets.map(target => target.replace(/\./g, '\\.').replace(/\*/g, '.*')).join('|')})$`); | ||
let domains = new Set(); | ||
|
||
function isStatic(rule) { | ||
function isStatic (rule) { | ||
if (isTrivial(rule)) { | ||
for (let target of targets) { | ||
domains.add(target); | ||
|
@@ -181,9 +183,104 @@ files.fork().zipAll([ sources.fork(), rules ]).map(([name, source, ruleset]) => | |
|
||
domains = Array.from(domains); | ||
|
||
// It is assumed that if all securecookies are static, | ||
// they can be safely ignored. | ||
// | ||
// A securecookie is called to be static either it is a trivial securecookie | ||
// or ALL of the following conditions are satisfied: | ||
// | ||
// 1. securecookie.host match cookie.host from the beginning ^ to the end $. | ||
// Otherwise, it might match subdomains/ partial patterns, thus a non-trivial | ||
// securecookie. | ||
// | ||
// 2. securecookie.host will not throw an error when passed to explodeRegExp(). | ||
// Otherwise, it might match patterns too complicated for our interests. | ||
// | ||
// 3. Each exploded securecookie.host should be included in ruleset.target/ | ||
// exploded target. Otherwise, this ruleset is likely problematic itself. It | ||
// is dangerous for a rewrite. | ||
function isStaticCookie (securecookie) { | ||
if (securecookie.host === '.+' && securecookie.name === '.+') { | ||
return [true, false]; | ||
} | ||
|
||
if (!securecookie.host.startsWith('^') || !securecookie.host.endsWith('$')) { | ||
return [false, false]; | ||
} | ||
|
||
let localDomains = new Set(); | ||
let unsupportedDomains = new Set(); | ||
|
||
try { | ||
explodeRegExp(securecookie.host, domain => { | ||
if (domain.startsWith('.')) { | ||
domain = domain.slice(1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this sufficient munging to ensure it matches the target domains? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Hainish this should be good enough. As mentioned in #16029 (comment), securecookie.host only match cookie.domain if there is a rule covers cookie.domain. Given the With the above logic in mind, I have also add 4bbd731 to remove some of these securecookie rules that will never be effective. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
localDomains.add(domain); | ||
}); | ||
} catch (e) { | ||
if (!(e instanceof UnsupportedRegExp)) { | ||
throw e; | ||
} | ||
warn`Unsupported regexp part ${e.message} while traversing securecookie : ${JSON.stringify(securecookie)}`; | ||
return [false, false]; | ||
} | ||
|
||
for (const domain of localDomains) { | ||
if (domains.indexOf(domain) === -1) { | ||
warn`Ruleset does not cover target ${domain} for securecookie : ${JSON.stringify(securecookie)}`; | ||
unsupportedDomains.add(domain); | ||
} | ||
} | ||
|
||
// For cookies to be covered, there must be at least one rule covering the | ||
// same domain. This is guaranteed by safeToSecureCookie(cookie) in rules.js | ||
// | ||
// Since securecookie.host will only match cookie.domain if there there is | ||
// a rule covers cookie.domain. Given the target are trivial, cookie.domain | ||
// cannot be anything other than domain.example.com and .domain.example.com | ||
// (possibly with more leading dots) for a securecookie rule ever to take | ||
// place. | ||
// | ||
// With condition (1) effective, securecookie.host should explode to either | ||
// one of the aforementioned patterns. Otherwise, the securecookie rules | ||
// will never be applied. Such dangling securecookie rules can be removed | ||
// safely. | ||
if (unsupportedDomains.size > 0) { | ||
if (unsupportedDomains.size === localDomains.size) { | ||
return [true, true]; | ||
} | ||
// TODO: Can we remove partial dangling securecookie rules??? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't think so. This means that the author intended at least some of the target domains to use secure cookies, we shouldn't throw out that information |
||
fail`Tag securecookie ${JSON.stringify(securecookie)} matches ${unsupportedDomains} which are not in targets ${targets}`; | ||
} | ||
return [true, false]; | ||
} | ||
|
||
if (domains.slice().sort().join('\n') !== targets.sort().join('\n')) { | ||
if (ruleset.securecookie) { | ||
return; | ||
// For each securecookie rule, check if it is a static securecookie. | ||
// If it is non-static, remove the securecookie rule if it contains | ||
// dangling rules. This removal is better done one by one to avoid | ||
// unwanted side effects. | ||
// Else if ALL securecookie rules are static, trivialize the targets. | ||
for (const securecookie of securecookies) { | ||
let [isStatic, shouldRemove] = isStaticCookie(securecookie); | ||
|
||
if (isStatic) { | ||
if (shouldRemove) { | ||
let scReSrc = `\n([\t ]*)<securecookie\\s*host=\\s*"${escapeStringRegexp(securecookie.host)}"(\\s*)name=\\s*"${escapeStringRegexp(securecookie.name)}"\\s*?/>[\t ]*\n`; | ||
let scRe = new RegExp(scReSrc); | ||
|
||
if (scRe && scRe.test(source)) { | ||
source = source.replace(scRe, ''); | ||
} else { | ||
fail`Failed to construct regexp which matches securecookie: ${JSON.stringify(securecookie)}`; | ||
return; | ||
} | ||
} | ||
} else { | ||
// Skip this ruleset as it contain non-static securecookies | ||
return; | ||
} | ||
} | ||
|
||
source = replaceXML(source, 'target', domains.map(domain => `<target host="${domain}" />`)); | ||
|
@@ -194,7 +291,6 @@ files.fork().zipAll([ sources.fork(), rules ]).map(([name, source, ruleset]) => | |
info`trivialized`; | ||
|
||
return writeFile(`${rulesDir}/${name}`, source); | ||
|
||
}) | ||
.filter(Boolean) | ||
.parallel(10) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, this file was previously formatted with prettier. Did it add these whitespaces or did you manually? Looks quite odd.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May I know which prettier options did you use? I can revert the changes in a37df21
P.S. a37df21 was mostly done automatically using
semistandard
from standardjsThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think just single-quotes plus semicolons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to me that prettier will create some strange changes to
trivialize-rules.js
, e.g. spliting the back-tick quoted string to the followingSo I guess it is better not to run prettier on it. Would you mind if I leave the file as-is in e8f27ee?