summaryrefslogtreecommitdiff
path: root/src/xss
diff options
context:
space:
mode:
authorhackademix2018-07-01 01:01:23 +0200
committerhackademix2018-07-01 01:01:23 +0200
commiteceae7187a6f0e9510bc1165f6977256b87f490f (patch)
treed943f1ec73c09efa70954dcedb55eac82a726148 /src/xss
downloadnoscript-eceae7187a6f0e9510bc1165f6977256b87f490f.tar.gz
noscript-eceae7187a6f0e9510bc1165f6977256b87f490f.tar.xz
noscript-eceae7187a6f0e9510bc1165f6977256b87f490f.zip
Initial commit starting at version 10.1.8.3rc4.
Diffstat (limited to 'src/xss')
-rw-r--r--src/xss/ASPIdiocy.js638
-rw-r--r--src/xss/Exceptions.js238
-rw-r--r--src/xss/FlashIdiocy.js147
-rw-r--r--src/xss/InjectionChecker.js1199
-rw-r--r--src/xss/XSS.js246
-rw-r--r--src/xss/sanitizeName.js4
6 files changed, 2472 insertions, 0 deletions
diff --git a/src/xss/ASPIdiocy.js b/src/xss/ASPIdiocy.js
new file mode 100644
index 0000000..c9958f1
--- /dev/null
+++ b/src/xss/ASPIdiocy.js
@@ -0,0 +1,638 @@
+'use strict';
+
+var ASPIdiocy = XSS.ASPIdiocy = {
+ _replaceRx: /%u([0-9a-fA-F]{4})/g,
+ _affectsRx: /%u[0-9a-fA-F]{4}/,
+ _badPercentRx: /%(?!u[0-9a-fA-F]{4}|[0-9a-fA-F]{2})|%(?:00|u0000)[^&=]*/g,
+
+ hasBadPercents(s) {
+ return this._badPercentRx.test(s)
+ },
+ removeBadPercents(s) {
+ return s.replace(this._badPercentRx, '');
+ },
+ affects(s) {
+ return this._affectsRx.test(s);
+ },
+ process(s) {
+ s = this.filter(s);
+ return /[\uff5f-\uffff]/.test(s) ? s + '&' + s.replace(/[\uff5f-\uffff]/g, '?') : s;
+ },
+ filter(s) {
+ return this.removeBadPercents(s).replace(this._replaceRx, this._replace)
+ },
+
+ coalesceQuery(s) { // HPP protection, see https://www.owasp.org/images/b/ba/AppsecEU09_CarettoniDiPaola_v0.8.pdf
+ let qm = s.indexOf("?");
+ if (qm < 0) return s;
+ let p = s.substring(0, qm);
+ let q = s.substring(qm + 1);
+ if (!q) return s;
+
+ let unchanged = true;
+ let emptyParams = false;
+
+ let pairs = (function rearrange(joinNames) {
+ let pairs = q.split("&");
+ let accumulator = {
+ __proto__: null
+ };
+ for (let j = 0, len = pairs.length; j < len; j++) {
+ let nv = pairs[j];
+ let eq = nv.indexOf("=");
+ if (eq === -1) {
+ emptyParams = true;
+ if (joinNames && j < len - 1) {
+ pairs[j + 1] = nv + "&" + pairs[j + 1];
+ delete pairs[j];
+ }
+ continue;
+ }
+ let key = "#" + unescape(nv.substring(0, eq)).toLowerCase();
+ if (key in accumulator) {
+ delete pairs[j];
+ pairs[accumulator[key]] += ", " + nv.substring(eq + 1);
+ unchanged = false;
+ } else {
+ accumulator[key] = j;
+ }
+ }
+ return (emptyParams && !(unchanged || joinNames)) ?
+ pairs.concat(rearrange(true).filter(p => pairs.indexOf(p) === -1)) :
+ pairs;
+ })();
+
+ if (unchanged) return s;
+ for (let j = pairs.length; j-- > 0;)
+ if (!pairs[j]) pairs.splice(j, 1);
+ return p + pairs.join("&");
+ },
+
+ _replace(match, hex) {
+ const k = parseInt(hex, 16);
+ const map = ASPIdiocy.map;
+ if (k in map) return map[k];
+ const range = ASPIdiocy._findRange(k);
+ return range && range.data || String.fromCharCode(k);
+ },
+ _findRange(k) {
+ const ranges = this.ranges;
+ for (let low = 0, high = ranges.length - 1; low <= high;) {
+ let i = parseInt((low + high) / 2);
+ let r = ranges[i];
+ let comparison = k < r.start ? 1 : k > r.end ? -1 : 0;
+ if (comparison < 0) low = i + 1;
+ else if (comparison > 0) high = i - 1;
+ else return r;
+ }
+ return null;
+ }
+}
+
+XSS.ASPIdiocy.map = {
+ 0x100: "\x41",
+ 0x101: "\x61",
+ 0x102: "\x41",
+ 0x103: "\x61",
+ 0x104: "\x41",
+ 0x105: "\x61",
+ 0x106: "\x43",
+ 0x107: "\x63",
+ 0x108: "\x43",
+ 0x109: "\x63",
+ 0x10a: "\x43",
+ 0x10b: "\x63",
+ 0x10c: "\x43",
+ 0x10d: "\x63",
+ 0x10e: "\x44",
+ 0x10f: "\x64",
+ 0x110: "\ufffd",
+ 0x111: "\x64",
+ 0x112: "\x45",
+ 0x113: "\x65",
+ 0x114: "\x45",
+ 0x115: "\x65",
+ 0x116: "\x45",
+ 0x117: "\x65",
+ 0x118: "\x45",
+ 0x119: "\x65",
+ 0x11a: "\x45",
+ 0x11b: "\x65",
+ 0x11c: "\x47",
+ 0x11d: "\x67",
+ 0x11e: "\x47",
+ 0x11f: "\x67",
+ 0x120: "\x47",
+ 0x121: "\x67",
+ 0x122: "\x47",
+ 0x123: "\x67",
+ 0x124: "\x48",
+ 0x125: "\x68",
+ 0x126: "\x48",
+ 0x127: "\x68",
+ 0x128: "\x49",
+ 0x129: "\x69",
+ 0x12a: "\x49",
+ 0x12b: "\x69",
+ 0x12c: "\x49",
+ 0x12d: "\x69",
+ 0x12e: "\x49",
+ 0x12f: "\x69",
+ 0x130: "\x49",
+ 0x131: "\x69",
+ 0x134: "\x4a",
+ 0x135: "\x6a",
+ 0x136: "\x4b",
+ 0x137: "\x6b",
+ 0x138: "\x3f",
+ 0x139: "\x4c",
+ 0x13a: "\x6c",
+ 0x13b: "\x4c",
+ 0x13c: "\x6c",
+ 0x13d: "\x4c",
+ 0x13e: "\x6c",
+ 0x141: "\x4c",
+ 0x142: "\x6c",
+ 0x143: "\x4e",
+ 0x144: "\x6e",
+ 0x145: "\x4e",
+ 0x146: "\x6e",
+ 0x147: "\x4e",
+ 0x148: "\x6e",
+ 0x14c: "\x4f",
+ 0x14d: "\x6f",
+ 0x14e: "\x4f",
+ 0x14f: "\x6f",
+ 0x150: "\x4f",
+ 0x151: "\x6f",
+ 0x154: "\x52",
+ 0x155: "\x72",
+ 0x156: "\x52",
+ 0x157: "\x72",
+ 0x158: "\x52",
+ 0x159: "\x72",
+ 0x15a: "\x53",
+ 0x15b: "\x73",
+ 0x15c: "\x53",
+ 0x15d: "\x73",
+ 0x15e: "\x53",
+ 0x15f: "\x73",
+ 0x162: "\x54",
+ 0x163: "\x74",
+ 0x164: "\x54",
+ 0x165: "\x74",
+ 0x166: "\x54",
+ 0x167: "\x74",
+ 0x168: "\x55",
+ 0x169: "\x75",
+ 0x16a: "\x55",
+ 0x16b: "\x75",
+ 0x16c: "\x55",
+ 0x16d: "\x75",
+ 0x16e: "\x55",
+ 0x16f: "\x75",
+ 0x170: "\x55",
+ 0x171: "\x75",
+ 0x172: "\x55",
+ 0x173: "\x75",
+ 0x174: "\x57",
+ 0x175: "\x77",
+ 0x176: "\x59",
+ 0x177: "\x79",
+ 0x178: "\ufffd",
+ 0x179: "\x5a",
+ 0x17a: "\x7a",
+ 0x17b: "\x5a",
+ 0x17c: "\x7a",
+ 0x17f: "\x3f",
+ 0x180: "\x62",
+ 0x189: "\ufffd",
+ 0x197: "\x49",
+ 0x19a: "\x6c",
+ 0x1a1: "\x6f",
+ 0x1ab: "\x74",
+ 0x1ae: "\x54",
+ 0x1af: "\x55",
+ 0x1b0: "\x75",
+ 0x1b6: "\x7a",
+ 0x1c0: "\x7c",
+ 0x1c3: "\x21",
+ 0x1cd: "\x41",
+ 0x1ce: "\x61",
+ 0x1cf: "\x49",
+ 0x1d0: "\x69",
+ 0x1d1: "\x4f",
+ 0x1d2: "\x6f",
+ 0x1d3: "\x55",
+ 0x1d4: "\x75",
+ 0x1d5: "\x55",
+ 0x1d6: "\x75",
+ 0x1d7: "\x55",
+ 0x1d8: "\x75",
+ 0x1d9: "\x55",
+ 0x1da: "\x75",
+ 0x1db: "\x55",
+ 0x1dc: "\x75",
+ 0x1dd: "\x3f",
+ 0x1de: "\x41",
+ 0x1df: "\x61",
+ 0x1e4: "\x47",
+ 0x1e5: "\x67",
+ 0x1e6: "\x47",
+ 0x1e7: "\x67",
+ 0x1e8: "\x4b",
+ 0x1e9: "\x6b",
+ 0x1ea: "\x4f",
+ 0x1eb: "\x6f",
+ 0x1ec: "\x4f",
+ 0x1ed: "\x6f",
+ 0x1f0: "\x6a",
+ 0x261: "\x67",
+ 0x2b9: "\x27",
+ 0x2ba: "\x22",
+ 0x2bb: "\x3f",
+ 0x2bc: "\x27",
+ 0x2c4: "\x5e",
+ 0x2c5: "\x3f",
+ 0x2c6: "\ufffd",
+ 0x2c7: "\x3f",
+ 0x2c8: "\x27",
+ 0x2cb: "\x60",
+ 0x2cc: "\x3f",
+ 0x2cd: "\x5f",
+ 0x2da: "\ufffd",
+ 0x2db: "\x3f",
+ 0x2dc: "\ufffd",
+ 0x300: "\x60",
+ 0x301: "\ufffd",
+ 0x302: "\x5e",
+ 0x303: "\x7e",
+ 0x308: "\ufffd",
+ 0x309: "\x3f",
+ 0x30a: "\ufffd",
+ 0x30e: "\x22",
+ 0x327: "\ufffd",
+ 0x37e: "\x3b",
+ 0x393: "\x47",
+ 0x398: "\x54",
+ 0x3a3: "\x53",
+ 0x3a6: "\x46",
+ 0x3a9: "\x4f",
+ 0x3b1: "\x61",
+ 0x3b2: "\ufffd",
+ 0x3b3: "\x3f",
+ 0x3b4: "\x64",
+ 0x3b5: "\x65",
+ 0x3bc: "\ufffd",
+ 0x3c0: "\x70",
+ 0x3c3: "\x73",
+ 0x3c4: "\x74",
+ 0x3c5: "\x3f",
+ 0x3c6: "\x66",
+ 0x4bb: "\x68",
+ 0x589: "\x3a",
+ 0x66a: "\x25",
+ 0x2012: "\x3f",
+ 0x2017: "\x3d",
+ 0x201b: "\x3f",
+ 0x201f: "\x3f",
+ 0x2023: "\x3f",
+ 0x2024: "\ufffd",
+ 0x2025: "\x3f",
+ 0x2026: "\ufffd",
+ 0x2030: "\ufffd",
+ 0x2031: "\x3f",
+ 0x2032: "\x27",
+ 0x2035: "\x60",
+ 0x2044: "\x2f",
+ 0x2070: "\ufffd",
+ 0x2074: "\x34",
+ 0x2075: "\x35",
+ 0x2076: "\x36",
+ 0x2077: "\x37",
+ 0x2078: "\x38",
+ 0x207f: "\x6e",
+ 0x2080: "\x30",
+ 0x2081: "\x31",
+ 0x2082: "\x32",
+ 0x2083: "\x33",
+ 0x2084: "\x34",
+ 0x2085: "\x35",
+ 0x2086: "\x36",
+ 0x2087: "\x37",
+ 0x2088: "\x38",
+ 0x2089: "\x39",
+ 0x20a1: "\ufffd",
+ 0x20a4: "\ufffd",
+ 0x20a7: "\x50",
+ 0x20ac: "\ufffd",
+ 0x2102: "\x43",
+ 0x2107: "\x45",
+ 0x210a: "\x67",
+ 0x210e: "\x68",
+ 0x210f: "\x3f",
+ 0x2112: "\x4c",
+ 0x2113: "\x6c",
+ 0x2114: "\x3f",
+ 0x2115: "\x4e",
+ 0x211a: "\x51",
+ 0x2122: "\ufffd",
+ 0x2123: "\x3f",
+ 0x2124: "\x5a",
+ 0x2128: "\x5a",
+ 0x2129: "\x3f",
+ 0x212a: "\x4b",
+ 0x212b: "\ufffd",
+ 0x212c: "\x42",
+ 0x212d: "\x43",
+ 0x2130: "\x45",
+ 0x2131: "\x46",
+ 0x2132: "\x3f",
+ 0x2133: "\x4d",
+ 0x2134: "\x6f",
+ 0x2205: "\ufffd",
+ 0x2212: "\x2d",
+ 0x2213: "\ufffd",
+ 0x2214: "\x3f",
+ 0x2215: "\x2f",
+ 0x2216: "\x5c",
+ 0x2217: "\x2a",
+ 0x221a: "\x76",
+ 0x221e: "\x38",
+ 0x2223: "\x7c",
+ 0x2229: "\x6e",
+ 0x2236: "\x3a",
+ 0x223c: "\x7e",
+ 0x2248: "\ufffd",
+ 0x2261: "\x3d",
+ 0x22c5: "\ufffd",
+ 0x2302: "\ufffd",
+ 0x2303: "\x5e",
+ 0x2310: "\ufffd",
+ 0x2320: "\x28",
+ 0x2321: "\x29",
+ 0x2329: "\x3c",
+ 0x232a: "\x3e",
+ 0x2500: "\x2d",
+ 0x2501: "\x3f",
+ 0x2502: "\ufffd",
+ 0x250c: "\x2b",
+ 0x2510: "\x2b",
+ 0x2514: "\x2b",
+ 0x2518: "\x2b",
+ 0x251c: "\x2b",
+ 0x2524: "\ufffd",
+ 0x252c: "\x2d",
+ 0x2534: "\x2d",
+ 0x253c: "\x2b",
+ 0x2550: "\x2d",
+ 0x2551: "\ufffd",
+ 0x2580: "\ufffd",
+ 0x2584: "\x5f",
+ 0x2588: "\ufffd",
+ 0x258c: "\ufffd",
+ 0x25a0: "\ufffd",
+ 0x263c: "\ufffd",
+ 0x2758: "\x7c",
+ 0x3000: "\x20",
+ 0x3008: "\x3c",
+ 0x3009: "\x3e",
+ 0x301a: "\x5b",
+ 0x301b: "\x5d",
+ 0x30fb: "\ufffd",
+ 0xff01: "\x21",
+ 0xff02: "\x22",
+ 0xff03: "\x23",
+ 0xff04: "\x24",
+ 0xff05: "\x25",
+ 0xff06: "\x26",
+ 0xff07: "\x27",
+ 0xff08: "\x28",
+ 0xff09: "\x29",
+ 0xff0a: "\x2a",
+ 0xff0b: "\x2b",
+ 0xff0c: "\x2c",
+ 0xff0d: "\x2d",
+ 0xff0e: "\x2e",
+ 0xff0f: "\x2f",
+ 0xff10: "\x30",
+ 0xff11: "\x31",
+ 0xff12: "\x32",
+ 0xff13: "\x33",
+ 0xff14: "\x34",
+ 0xff15: "\x35",
+ 0xff16: "\x36",
+ 0xff17: "\x37",
+ 0xff18: "\x38",
+ 0xff19: "\x39",
+ 0xff1a: "\x3a",
+ 0xff1b: "\x3b",
+ 0xff1c: "\x3c",
+ 0xff1d: "\x3d",
+ 0xff1e: "\x3e",
+ 0xff1f: "\x3f",
+ 0xff20: "\x40",
+ 0xff21: "\x41",
+ 0xff22: "\x42",
+ 0xff23: "\x43",
+ 0xff24: "\x44",
+ 0xff25: "\x45",
+ 0xff26: "\x46",
+ 0xff27: "\x47",
+ 0xff28: "\x48",
+ 0xff29: "\x49",
+ 0xff2a: "\x4a",
+ 0xff2b: "\x4b",
+ 0xff2c: "\x4c",
+ 0xff2d: "\x4d",
+ 0xff2e: "\x4e",
+ 0xff2f: "\x4f",
+ 0xff30: "\x50",
+ 0xff31: "\x51",
+ 0xff32: "\x52",
+ 0xff33: "\x53",
+ 0xff34: "\x54",
+ 0xff35: "\x55",
+ 0xff36: "\x56",
+ 0xff37: "\x57",
+ 0xff38: "\x58",
+ 0xff39: "\x59",
+ 0xff3a: "\x5a",
+ 0xff3b: "\x5b",
+ 0xff3c: "\x5c",
+ 0xff3d: "\x5d",
+ 0xff3e: "\x5e",
+ 0xff3f: "\x5f",
+ 0xff40: "\x60",
+ 0xff41: "\x61",
+ 0xff42: "\x62",
+ 0xff43: "\x63",
+ 0xff44: "\x64",
+ 0xff45: "\x65",
+ 0xff46: "\x66",
+ 0xff47: "\x67",
+ 0xff48: "\x68",
+ 0xff49: "\x69",
+ 0xff4a: "\x6a",
+ 0xff4b: "\x6b",
+ 0xff4c: "\x6c",
+ 0xff4d: "\x6d",
+ 0xff4e: "\x6e",
+ 0xff4f: "\x6f",
+ 0xff50: "\x70",
+ 0xff51: "\x71",
+ 0xff52: "\x72",
+ 0xff53: "\x73",
+ 0xff54: "\x74",
+ 0xff55: "\x75",
+ 0xff56: "\x76",
+ 0xff57: "\x77",
+ 0xff58: "\x78",
+ 0xff59: "\x79",
+ 0xff5a: "\x7a",
+ 0xff5b: "\x7b",
+ 0xff5c: "\x7c",
+ 0xff5d: "\x7d",
+ 0xff5e: "\x7e"
+};
+
+{
+ let Range = class {
+ constructor(start, end, data) {
+ this.start = start;
+ this.end = end;
+ this.data = data;
+ }
+ };
+
+ XSS.ASPIdiocy.ranges = [
+ new Range(0x80, 0xff, "\ufffd"),
+ new Range(0x132, 0x133, "\x3f"),
+ new Range(0x13f, 0x140, "\x3f"),
+ new Range(0x149, 0x14b, "\x3f"),
+ new Range(0x152, 0x153, "\ufffd"),
+ new Range(0x160, 0x161, "\ufffd"),
+ new Range(0x17d, 0x17e, "\ufffd"),
+ new Range(0x181, 0x188, "\x3f"),
+ new Range(0x18a, 0x190, "\x3f"),
+ new Range(0x191, 0x192, "\ufffd"),
+ new Range(0x193, 0x196, "\x3f"),
+ new Range(0x198, 0x199, "\x3f"),
+ new Range(0x19b, 0x19e, "\x3f"),
+ new Range(0x19f, 0x1a0, "\x4f"),
+ new Range(0x1a2, 0x1aa, "\x3f"),
+ new Range(0x1ac, 0x1ad, "\x3f"),
+ new Range(0x1b1, 0x1b5, "\x3f"),
+ new Range(0x1b7, 0x1bf, "\x3f"),
+ new Range(0x1c1, 0x1c2, "\x3f"),
+ new Range(0x1c4, 0x1cc, "\x3f"),
+ new Range(0x1e0, 0x1e3, "\x3f"),
+ new Range(0x1ee, 0x1ef, "\x3f"),
+ new Range(0x1f1, 0x260, "\x3f"),
+ new Range(0x262, 0x2b8, "\x3f"),
+ new Range(0x2bd, 0x2c3, "\x3f"),
+ new Range(0x2c9, 0x2ca, "\ufffd"),
+ new Range(0x2ce, 0x2d9, "\x3f"),
+ new Range(0x2dd, 0x2ff, "\x3f"),
+ new Range(0x304, 0x305, "\ufffd"),
+ new Range(0x306, 0x307, "\x3f"),
+ new Range(0x30b, 0x30d, "\x3f"),
+ new Range(0x30f, 0x326, "\x3f"),
+ new Range(0x328, 0x330, "\x3f"),
+ new Range(0x331, 0x332, "\x5f"),
+ new Range(0x333, 0x37d, "\x3f"),
+ new Range(0x37f, 0x392, "\x3f"),
+ new Range(0x394, 0x397, "\x3f"),
+ new Range(0x399, 0x3a2, "\x3f"),
+ new Range(0x3a4, 0x3a5, "\x3f"),
+ new Range(0x3a7, 0x3a8, "\x3f"),
+ new Range(0x3aa, 0x3b0, "\x3f"),
+ new Range(0x3b6, 0x3bb, "\x3f"),
+ new Range(0x3bd, 0x3bf, "\x3f"),
+ new Range(0x3c1, 0x3c2, "\x3f"),
+ new Range(0x3c7, 0x4ba, "\x3f"),
+ new Range(0x4bc, 0x588, "\x3f"),
+ new Range(0x58a, 0x669, "\x3f"),
+ new Range(0x66b, 0x1fff, "\x3f"),
+ new Range(0x2000, 0x2006, "\x20"),
+ new Range(0x2007, 0x200f, "\x3f"),
+ new Range(0x2010, 0x2011, "\x2d"),
+ new Range(0x2013, 0x2014, "\ufffd"),
+ new Range(0x2015, 0x2016, "\x3f"),
+ new Range(0x2018, 0x201a, "\ufffd"),
+ new Range(0x201c, 0x201e, "\ufffd"),
+ new Range(0x2020, 0x2022, "\ufffd"),
+ new Range(0x2027, 0x202f, "\x3f"),
+ new Range(0x2033, 0x2034, "\x3f"),
+ new Range(0x2036, 0x2038, "\x3f"),
+ new Range(0x2039, 0x203a, "\ufffd"),
+ new Range(0x203b, 0x2043, "\x3f"),
+ new Range(0x2045, 0x206f, "\x3f"),
+ new Range(0x2071, 0x2073, "\x3f"),
+ new Range(0x2079, 0x207e, "\x3f"),
+ new Range(0x208a, 0x20a0, "\x3f"),
+ new Range(0x20a2, 0x20a3, "\x3f"),
+ new Range(0x20a5, 0x20a6, "\x3f"),
+ new Range(0x20a8, 0x20ab, "\x3f"),
+ new Range(0x20ad, 0x2101, "\x3f"),
+ new Range(0x2103, 0x2106, "\x3f"),
+ new Range(0x2108, 0x2109, "\x3f"),
+ new Range(0x210b, 0x210d, "\x48"),
+ new Range(0x2110, 0x2111, "\x49"),
+ new Range(0x2116, 0x2117, "\x3f"),
+ new Range(0x2118, 0x2119, "\x50"),
+ new Range(0x211b, 0x211d, "\x52"),
+ new Range(0x211e, 0x2121, "\x3f"),
+ new Range(0x2125, 0x2127, "\x3f"),
+ new Range(0x212e, 0x212f, "\x65"),
+ new Range(0x2135, 0x2204, "\x3f"),
+ new Range(0x2206, 0x2211, "\x3f"),
+ new Range(0x2218, 0x2219, "\ufffd"),
+ new Range(0x221b, 0x221d, "\x3f"),
+ new Range(0x221f, 0x2222, "\x3f"),
+ new Range(0x2224, 0x2228, "\x3f"),
+ new Range(0x222a, 0x2235, "\x3f"),
+ new Range(0x2237, 0x223b, "\x3f"),
+ new Range(0x223d, 0x2247, "\x3f"),
+ new Range(0x2249, 0x2260, "\x3f"),
+ new Range(0x2262, 0x2263, "\x3f"),
+ new Range(0x2264, 0x2265, "\x3d"),
+ new Range(0x2266, 0x2269, "\x3f"),
+ new Range(0x226a, 0x226b, "\ufffd"),
+ new Range(0x226c, 0x22c4, "\x3f"),
+ new Range(0x22c6, 0x2301, "\x3f"),
+ new Range(0x2304, 0x230f, "\x3f"),
+ new Range(0x2311, 0x231f, "\x3f"),
+ new Range(0x2322, 0x2328, "\x3f"),
+ new Range(0x232b, 0x24ff, "\x3f"),
+ new Range(0x2503, 0x250b, "\x3f"),
+ new Range(0x250d, 0x250f, "\x3f"),
+ new Range(0x2511, 0x2513, "\x3f"),
+ new Range(0x2515, 0x2517, "\x3f"),
+ new Range(0x2519, 0x251b, "\x3f"),
+ new Range(0x251d, 0x2523, "\x3f"),
+ new Range(0x2525, 0x252b, "\x3f"),
+ new Range(0x252d, 0x2533, "\x3f"),
+ new Range(0x2535, 0x253b, "\x3f"),
+ new Range(0x253d, 0x254f, "\x3f"),
+ new Range(0x2552, 0x255d, "\x2b"),
+ new Range(0x255e, 0x2563, "\ufffd"),
+ new Range(0x2564, 0x2569, "\x2d"),
+ new Range(0x256a, 0x256c, "\x2b"),
+ new Range(0x256d, 0x257f, "\x3f"),
+ new Range(0x2581, 0x2583, "\x3f"),
+ new Range(0x2585, 0x2587, "\x3f"),
+ new Range(0x2589, 0x258b, "\x3f"),
+ new Range(0x258d, 0x258f, "\x3f"),
+ new Range(0x2590, 0x2593, "\ufffd"),
+ new Range(0x2594, 0x259f, "\x3f"),
+ new Range(0x25a1, 0x263b, "\x3f"),
+ new Range(0x263d, 0x2757, "\x3f"),
+ new Range(0x2759, 0x2fff, "\x3f"),
+ new Range(0x3001, 0x3007, "\x3f"),
+ new Range(0x300a, 0x300b, "\ufffd"),
+ new Range(0x300c, 0x3019, "\x3f"),
+ new Range(0x301c, 0x30fa, "\x3f"),
+ new Range(0x30fc, 0xff00, "\x3f")
+ ];
+}
diff --git a/src/xss/Exceptions.js b/src/xss/Exceptions.js
new file mode 100644
index 0000000..4e80c39
--- /dev/null
+++ b/src/xss/Exceptions.js
@@ -0,0 +1,238 @@
+'use strict';
+
+XSS.Exceptions = (() => {
+
+ var Exceptions = {
+ get legacyExceptions() {
+ delete this.legacyExceptions;
+ this.legacyExceptions =
+ Legacy.getRxPref("filterXExceptions",
+ Legacy.RX.multi, "g", /^https?:[a-z:/@.?-]*$/i);
+ return this.legacyExceptions;
+ },
+
+ async getWhitelist() {
+ return (await Storage.get("sync", "xssWhitelist")).xssWhitelist;
+ },
+ async setWhitelist(xssWhitelist) {
+ await Storage.set("sync", {xssWhitelist});
+ },
+
+ async shouldIgnore(xssReq) {
+ function logEx(...args) {
+ debug("[XSS preprocessing] Ignoring %o", xssReq, ...args);
+ }
+
+ let {
+ srcObj,
+ destObj,
+ srcUrl,
+ destUrl,
+ srcOrigin,
+ destOrigin,
+ unescapedDest,
+ isGet,
+ isPost
+ } = xssReq;
+
+ // same srcUrl
+ if (srcOrigin === destOrigin) {
+ return true;
+ }
+
+ // same domain + https: source
+ if (/^https:/.test(srcOrigin) && xssReq.srcDomain === xssReq.destDomain) {
+ return true;
+ }
+
+ if (/^(?:chrome|resource|moz-extension|about):/.test(srcOrigin)) {
+ debug("Privileged origin", srcOrigin);
+ }
+
+ // destination or @source matching legacy regexp
+ if (this.legacyExceptions.test(unescapedDest) &&
+ !this.isBadException(destObj.hostname) ||
+ this.legacyExceptions.test("@" + unescape(srcUrl))) {
+ logEx("Legacy exception", this.legacyExceptions);
+ return true;
+ }
+
+ if (!srcObj && isGet) {
+ if (/^https?:\/\/msdn\.microsoft\.com\/query\/[^<]+$/.test(unescapedDest)) {
+ return true; // MSDN from Microsoft VS
+ }
+ }
+
+ if (srcOrigin) { // srcUrl-specific exceptions
+
+ if (/^about:(?!blank)/.test(srcOrigin))
+ return true; // any about: URL except about:blank
+
+ if (srcOrigin === "https://www.youtube.com" &&
+ /^https:\/\/(?:plus\.googleapis|apis\.google)\.com\/[\w/]+\/widget\/render\/comments\?/.test(destUrl) &&
+ Legacy.getPref("filterXExceptions.yt_comments")
+ ) {
+ logEx("YouTube comments exception");
+ return true;
+ }
+
+ if (isPost) {
+
+ if (srcOrigin === "https://sso.post.ch" && destOrigin === "https://app.swisspost.ch") {
+ return true;
+ }
+
+ if (srcOrigin === "https://twitter.com" && /^https:\/\/.*\.twitter\.com$/.test(destOrigin)) {
+ return true;
+ }
+
+ {
+ let rx = /^https:\/\/(?:[a-z]+\.)?unionbank\.com$/;
+ if (rx.test(srcOrigin) && rx.test(destOrigin)) {
+ return true;
+ }
+ }
+
+ if (/^https?:\/\/csr\.ebay\.(?:\w{2,3}|co\.uk)\/cse\/start\.jsf$/.test(srcUrl) &&
+ /^https?:\/\/msa-lfn\.ebay\.(?:\w{2,3}|co\.uk)\/ws\/eBayISAPI\.dll\?[^<'"%]*$/.test(unescapedDest) &&
+ destObj.protocol === srcObj.protocol &&
+ Legacy.getPref("filterXException.ebay")) {
+ logEx("Ebay exception");
+ return true;
+ }
+
+ if (/^https:\/\/(?:cap\.securecode\.com|www\.securesuite\.net|(?:.*?\.)?firstdata\.(?:l[tv]|com))$/.test(srcUrl) &&
+ Legacy.getPref("filterXException.visa")) {
+ logEx("Verified by Visa exception");
+ return true;
+ }
+
+ if (/\.verizon\.com$/.test(srcOrigin) &&
+ /^https:\/\/signin\.verizon\.com\/sso\/authsso\/forumLogin\.jsp$/.test(destUrl) &&
+ Legacy.getPref("filterXExceptions.verizon")) {
+ logEx("Verizon login exception");
+ return true;
+ }
+
+ if (/^https?:\/\/mail\.lycos\.com\/lycos\/mail\/MailCompose\.lycos$/.test(srcUrl) &&
+ /\.lycosmail\.lycos\.com$/.test(destOrigin) &&
+ Legacy.getPref("filterXExceptions.lycosmail")) {
+ logEx("Lycos Mail exception");
+ return true;
+ }
+
+ if (/\.livejournal\.com$/.test(srcOrigin) &&
+ /^https?:\/\/www\.livejournal\.com\/talkpost_do\.bml$/.test(destUrl) &&
+ Legacy.getPref("filterXExceptions.livejournal")) {
+ logEx("Livejournal comments exception");
+ return true;
+ }
+
+ if (srcOrigin == "https://ssl.rapidshare.com" &&
+ xssReq.srcDomain == "rapidshare.com") {
+ logEx("Rapidshare upload exception");
+ return true;
+ }
+
+ if (srcOrigin == "http://wm.letitbit.net" &&
+ /^http:\/\/http\.letitbit\.net:81\/cgi-bin\/multi\/upload\.cgi\?/.test(destUrl) &&
+ Legacy.getPref("filterXExceptions.letitibit")
+ ) {
+ logEx("letitbit.net upload exception");
+ return true;
+ }
+
+ if (/\.deviantart\.com$/.test(srcOrigin) &&
+ /^http:\/\/my\.deviantart\.com\/journal\/update\b/.test(destUrl) &&
+ Legacy.getPref("filterXExceptions.deviantart")
+ ) {
+ logEx("deviantart.com journal post exception");
+ return true;
+ }
+
+ if (srcOrigin == "https://www.mymedicare.gov" &&
+ destOrigin == "https://myporal.medicare.gov" &&
+ Legacy.getPref("filterXExceptions.medicare")
+ ) {
+ logEx("mymedicare.gov exception");
+ return true;
+ }
+
+ if (/^https?:\/\/(?:draft|www)\.blogger\.com\/template-editor\.g\?/.test(srcUrl) &&
+ /^https?:\/\/[\w\-]+\.blogspot\.com\/b\/preview\?/.test(destUrl) &&
+ Legacy.getPref("filterXExceptions.blogspot")
+ ) {
+ logEx("blogspot.com template preview exception");
+ return true;
+ }
+
+ if (/^https?:\/\/www\.readability\.com\/articles\/queue$/.test(destUrl) &&
+ Legacy.getPref("filterXExceptions.readability")) {
+ logEx("Readability exception");
+ return true;
+ }
+
+ if (/^https?:\/\/pdf\.printfriendly\.com\/pdfs\/make$/.test(destUrl) &&
+ Legacy.getPref("filterXExceptions.printfriendly")) {
+ logEx("Printfriendly exception");
+ return true;
+ }
+ }
+ }
+ },
+
+ isBadException(host) {
+ // TLD check for Google search
+ let m = host.match(/\bgoogle\.((?:[a-z]{1,3}\.)?[a-z]+)$/i);
+ return m && tld.getPublicSuffix(host) != m[1];
+ },
+
+ partial(xssReq) {
+ let {
+ srcObj,
+ destObj,
+ srcUrl,
+ destUrl,
+ srcOrigin,
+ destOrigin,
+ } = xssReq;
+
+ let skipParams, skipRx;
+ if (/^https:\/\/www\.paypal\.com\/(?:[\w\-]+\/)?cgi-bin\/webscr\b/.test(destUrl)) {
+ // Paypal buttons encrypted parameter causes a DOS, strip it out
+ skipParams = ['encrypted'];
+ } else if (/\.adnxs\.com$/.test(srcOrigin) && /\.adnxs\.com$/.test(destOrigin)) {
+ skipParams = ['udj'];
+ } else if (/^https?:\/\/www\.mendeley\.com\/import\/bookmarklet\/$/.test(destUrl)) {
+ skipParams = ['html'];
+ } else if (destObj.hash && /^https:/.test(srcOrigin) &&
+ (/^https?:\/\/api\.facebook\.com\//.test(srcUrl) ||
+ /^https:\/\/tbpl\.mozilla\.org\//.test(srcUrl) || // work-around for hg reftest DOS
+ /^https:\/\/[^\/]+\.googleusercontent\.com\/gadgets\/ifr\?/.test(destUrl) // Google gadgets
+ )) {
+ skipRx = /#[^#]+$/; // remove receiver's hash
+ } else if (/^https?:\/\/apps\.facebook\.com\//.test(srcUrl) && Legacy.getPref("filterXExceptions.fbconnect")) {
+ skipRx = /&invite_url=javascript[^&]+/; // Zynga stuff
+ } else if (/^https?:\/\/l\.yimg\.com\/j\/static\/frame\?e=/.test(destUrl) &&
+ /\.yahoo\.com$/.test(srcOrigin) &&
+ Legacy.getPref("filterXExceptions.yahoo")) {
+ skipParams = ['e'];
+ } else if (/^https?:\/\/wpcomwidgets\.com\/\?/.test(destUrl)) {
+ skipParams = ["_data"];
+ } else if (/^https:\/\/docs\.google\.com\/picker\?/.test(destUrl)) {
+ skipParams = ["nav", "pp"];
+ } else if (/^https:\/\/.*[\?&]scope=/.test(destUrl)) {
+ skipRx = /[\?&]scope=[+\w]+(?=&|$)/;
+ }
+ if (skipParams) {
+ skipRx = new RegExp("(?:^|[&?])(?:" + skipParams.join('|') + ")=[^&]+", "g");
+ }
+ return {
+ skipParams,
+ skipRx
+ };
+ }
+
+ };
+ return Exceptions;
+})();
diff --git a/src/xss/FlashIdiocy.js b/src/xss/FlashIdiocy.js
new file mode 100644
index 0000000..c6835b1
--- /dev/null
+++ b/src/xss/FlashIdiocy.js
@@ -0,0 +1,147 @@
+'use strict';
+
+XSS.FlashIdiocy = {
+ _affectsRx: /%(?:[8-9a-f]|[0-7]?[^0-9a-f])/i, // high (non-ASCII) percent encoding or invalid second digit
+ affects(s) {
+ return this._affectsRx.test(s);
+ },
+
+ purgeBadEncodings(s) {
+ return s.replace(/%(?:[0-9a-f]?(?:[^0-9a-f]|$))/ig, "");
+ },
+
+ platformDecode(s) {
+ return s.replace(/%[8-9a-f][0-9a-f]/ig, s => this.map[s.substring(1).toLowerCase()]);
+ },
+
+ map: {
+ "80": "?",
+ "81": "",
+ "82": "?",
+ "83": "?",
+ "84": "?",
+ "85": "?",
+ "86": "?",
+ "87": "?",
+ "88": "?",
+ "89": "?",
+ "8a": "?",
+ "8b": "?",
+ "8c": "?",
+ "8d": "",
+ "8e": "?",
+ "8f": "",
+ "90": "",
+ "91": "?",
+ "92": "?",
+ "93": "?",
+ "94": "?",
+ "95": "?",
+ "96": "?",
+ "97": "?",
+ "98": "?",
+ "99": "?",
+ "9a": "?",
+ "9b": "?",
+ "9c": "?",
+ "9d": "",
+ "9e": "?",
+ "9f": "?",
+ "a0": " ",
+ "a1": "¡",
+ "a2": "¢",
+ "a3": "£",
+ "a4": "¤",
+ "a5": "¥",
+ "a6": "¦",
+ "a7": "§",
+ "a8": "¨",
+ "a9": "©",
+ "aa": "ª",
+ "ab": "«",
+ "ac": "¬",
+ "ad": "­",
+ "ae": "®",
+ "af": "¯",
+ "b0": "°",
+ "b1": "±",
+ "b2": "²",
+ "b3": "³",
+ "b4": "´",
+ "b5": "µ",
+ "b6": "¶",
+ "b7": "·",
+ "b8": "¸",
+ "b9": "¹",
+ "ba": "º",
+ "bb": "»",
+ "bc": "¼",
+ "bd": "½",
+ "be": "¾",
+ "bf": "¿",
+ "c0": "À",
+ "c1": "Á",
+ "c2": "Â",
+ "c3": "Ã",
+ "c4": "Ä",
+ "c5": "Å",
+ "c6": "Æ",
+ "c7": "Ç",
+ "c8": "È",
+ "c9": "É",
+ "ca": "Ê",
+ "cb": "Ë",
+ "cc": "Ì",
+ "cd": "Í",
+ "ce": "Î",
+ "cf": "Ï",
+ "d0": "Ð",
+ "d1": "Ñ",
+ "d2": "Ò",
+ "d3": "Ó",
+ "d4": "Ô",
+ "d5": "Õ",
+ "d6": "Ö",
+ "d7": "×",
+ "d8": "Ø",
+ "d9": "Ù",
+ "da": "Ú",
+ "db": "Û",
+ "dc": "Ü",
+ "dd": "Ý",
+ "de": "Þ",
+ "df": "ß",
+ "e0": "à",
+ "e1": "á",
+ "e2": "â",
+ "e3": "ã",
+ "e4": "ä",
+ "e5": "å",
+ "e6": "æ",
+ "e7": "ç",
+ "e8": "è",
+ "e9": "é",
+ "ea": "ê",
+ "eb": "ë",
+ "ec": "ì",
+ "ed": "í",
+ "ee": "î",
+ "ef": "ï",
+ "f0": "ð",
+ "f1": "ñ",
+ "f2": "ò",
+ "f3": "ó",
+ "f4": "ô",
+ "f5": "õ",
+ "f6": "ö",
+ "f7": "÷",
+ "f8": "ø",
+ "f9": "ù",
+ "fa": "ú",
+ "fb": "û",
+ "fc": "ü",
+ "fd": "ý",
+ "fe": "þ",
+ "ff": "ÿ",
+ }
+};
diff --git a/src/xss/InjectionChecker.js b/src/xss/InjectionChecker.js
new file mode 100644
index 0000000..9617e3d
--- /dev/null
+++ b/src/xss/InjectionChecker.js
@@ -0,0 +1,1199 @@
+debug("Initializing InjectionChecker");
+XSS.InjectionChecker = (async () => {
+ await include([
+ "/lib/Base64.js",
+ "/xss/FlashIdiocy.js",
+ "/xss/ASPIdiocy.js"]
+ );
+
+ var {FlashIdiocy, ASPIdiocy} = XSS;
+
+ const wordCharRx = /\w/g;
+
+ function fuzzify(s) {
+ return s.replace(wordCharRx, '\\W*(?:/[*/][^]*)?$&');
+ }
+
+ const IC_COMMENT_PATTERN = '\\s*(?:\\/[\\/\\*][^]+)?';
+ const IC_WINDOW_OPENER_PATTERN = fuzzify("alert|confirm|prompt|open(?:URL)?|print|show") + "\\w*" + fuzzify("Dialog");
+ const IC_EVAL_PATTERN = "\\b(?:" +
+ fuzzify('eval|import|set(?:Timeout|Interval)|(?:f|F)unction|Script|toString|Worker|document|constructor|generateCRMFRequest|jQuery|fetch|write(?:ln)?|__(?:define(?:S|G)etter|noSuchMethod)__|definePropert(?:y|ies)') +
+ "|\\$|" + IC_WINDOW_OPENER_PATTERN + ")\\b";
+ const IC_EVENT_PATTERN = "on(?:p(?:o(?:inter(?:l(?:ock(?:change|error)|eave)|o(?:ver|ut)|cancel|enter|down|move|up)|p(?:up(?:hid(?:den|ing)|show(?:ing|n)|positioned)|state))|a(?:ge(?:hide|show)|(?:st|us)e)|ush(?:subscriptionchange)?|ro(?:cessorerror|gress)|lay(?:ing)?|hoto)|Moz(?:S(?:wipeGesture(?:(?:May)?Start|Update|End)?|crolledAreaChanged)|M(?:agnifyGesture(?:Update|Start)?|ouse(?:PixelScroll|Hittest))|EdgeUI(?:C(?:omplet|ancel)|Start)ed|RotateGesture(?:Update|Start)?|(?:Press)?TapGesture|AfterPaint)|m(?:o(?:z(?:pointerlock(?:change|error)|fullscreen(?:change|error)|key(?:down|up)onplugin|accesskeynotfound|orientationchange)|use(?:l(?:ongtap|eave)|o(?:ver|ut)|enter|wheel|down|move|up))|(?:idimessag|ut)e|essage(?:error)?|ark)|c(?:o(?:m(?:p(?:osition(?:update|start|end)|lete)|mand(?:update)?)|n(?:t(?:rollerchange|extmenu)|nect(?:ionavailable)?)|py)|h(?:(?:arging(?:time)?ch)?ange|ecking)|a(?:n(?:play(?:through)?|cel)|ched)|u(?:echange|t)|l(?:ick|ose))|s(?:ou(?:rce(?:(?:clos|end)ed|open)|nd(?:start|end))|e(?:lect(?:ionchange|start)?|ek(?:ing|ed)|t)|h(?:ipping(?:address|option)change|ow)|t(?:a(?:techange|lled|rt)|o(?:rage|p))|u(?:ccess|spend|bmit)|peech(?:start|end)|croll)|d(?:r(?:a(?:g(?:e(?:n(?:ter|d)|xit)|leave|start|drop|over)?|in)|op)|evice(?:(?:orienta|mo)tion|proximity|change|light)|(?:ischargingtime|uration)change|ata(?:available)?|ownloading|blclick)|a(?:nimation(?:iteration|cancel|start|end)|u(?:dio(?:process|start|end)|xclick)|b(?:solutedeviceorientation|ort)|fter(?:scriptexecute|print)|dd(?:sourcebuffer|track)|ppinstalled|ctivate)|DOM(?:Node(?:Inserted(?:IntoDocument)?|Removed(?:FromDocument)?)|(?:CharacterData|Subtree)Modified|A(?:ttrModified|ctivate)|Focus(?:Out|In)|MouseScroll)|r(?:e(?:s(?:ourcetimingbufferfull|ponseprogress|u(?:lt|me)|ize|et)|move(?:sourcebuffer|track)|adystatechange|pea(?:tEven)?t|questprogress)|atechange)|w(?:ebkit(?:Animation(?:Iteration|Start|End)|animation(?:iteration|start|end)|(?:TransitionE|transitione)nd)|a(?:iting(?:forkey)?|rning)|heel)|v(?:rdisplay(?:(?:presentchang|activat)e|d(?:eactivate|isconnect)|connect)|o(?:iceschanged|lumechange)|(?:isibility|ersion)change)|b(?:e(?:fore(?:p(?:aste|rint)|scriptexecute|c(?:opy|ut)|unload)|gin(?:Event)?)|ufferedamountlow|l(?:ocked|ur)|roadcast|oundary)|t(?:o(?:uch(?:cancel|start|move|end)|ggle)|ransition(?:cancel|start|end|run)|ime(?:update|out)|e(?:rminate|xt)|ypechange)|l(?:o(?:ad(?:e(?:d(?:meta)?data|nd)|ing(?:error|done)?|start)?|stpointercapture)|(?:anguage|evel)change|y)|u(?:p(?:date(?:(?:fou|e)nd|ready|start)?|gradeneeded)|n(?:derflow|load|mute)|serproximity)|g(?:amepad(?:(?:dis)?connected|button(?:down|up)|axismove)|otpointercapture|et)|o(?:(?:rientationchang|(?:ff|n)lin|bsolet)e|verflow|pen)|e(?:n(?:d(?:Event|ed)?|crypted|ter)|mptied|rror|xit)|f(?:ullscreen(?:change|error)|ocus(?:out|in)?|inish)|no(?:tificationcl(?:ick|ose)|update|match)|SVG(?:(?:Unl|L)oad|Resize|Scroll|Zoom)|key(?:statuseschange|press|down|up)|(?:CheckboxStateC|hashc)hange|R(?:adioStateChange|equest)|in(?:stall|valid|put)|AppCommand|zoom)"
+ // autogenerated from nsGkAtomList.h
+ ;
+ const IC_EVENT_DOS_PATTERN =
+ "\\b(?:" + IC_EVENT_PATTERN + ")[^]*=[^]*\\b(?:" + IC_WINDOW_OPENER_PATTERN + ")\\b" +
+ "|\\b(?:" + IC_WINDOW_OPENER_PATTERN + ")\\b[^]+\\b(?:" + IC_EVENT_PATTERN + ")[^]*=";
+
+ var InjectionChecker = {
+ reset: function() {
+
+ this.isPost =
+ this.base64 =
+ this.nameAssignment = false;
+
+ this.base64tested = [];
+
+ },
+
+ fuzzify: fuzzify,
+ syntax: new SyntaxChecker(),
+ _log: function(msg, t, i) {
+ if (msg) msg = this._printable(msg);
+ if (t) msg += " - TIME: " + (Date.now() - t);
+ if (i) msg += " - ITER: " + i;
+ debug("[InjectionChecker]", msg);
+ },
+
+ _printable: function(msg) {
+ return msg.toString().replace(/[^\u0020-\u007e]/g, function(s) {
+ return "{" + s.charCodeAt(0).toString(16) + "}";
+ });
+ },
+ log: function() {},
+ get logEnabled() {
+ return this.log == this._log;
+ },
+ set logEnabled(v) {
+ this.log = v ? this._log : function() {};
+ },
+
+ escalate: function(msg) {
+ this.log(msg);
+ log("[InjectionChecker] ", msg);
+ },
+
+ bb: function(brac, s, kets) {
+ for (var j = 3; j-- > 0;) {
+ s = brac + s + kets;
+ if (this.checkJSSyntax(s)) return true;
+ }
+ return false;
+ },
+
+ checkJSSyntax(s) {
+ // bracket balancing for micro injections like "''), e v a l (name,''"
+ if (/^(?:''|"")?[^\('"]*\)/.test(s)) return this.bb("x(\n", s, "\n)");
+ if (/^(?:''|"")?[^\['"]*\\]/.test(s)) return this.bb("y[\n", s, "\n]");
+ if (/^(?:''|"")?[^\{'"]*\}/.test(s)) return this.bb("function z() {\n", s, "\n}");
+
+ let syntax = this.syntax;
+ s += " /* COMMENT_TERMINATOR */\nDUMMY_EXPR";
+ if (syntax.check(s)) {
+ this.log("Valid fragment " + s);
+ return true;
+ }
+ return false;
+ },
+
+ checkTemplates(script) {
+ let templateExpressions = script.replace(/[[\]{}]/g, ";");
+ return templateExpressions !== script &&
+ (this.maybeMavo(script) ||
+ (this.maybeJS(templateExpressions, true) &&
+ (this.syntax.check(templateExpressions) ||
+ /[^><=]=[^=]/.test(templateExpressions) && this.syntax.check(
+ templateExpressions.replace(/([^><=])=(?=[^=])/g, '$1=='))
+ )));
+ },
+
+ maybeMavo(s) {
+ return /\[[^]*\([^]*\)[^]*\]/.test(s) && /\b(?:and|or|mod|\$url\b)/.test(s) &&
+ this.maybeJS(s.replace(/\b(?:and|or|mod|[[\]])/g, ',').replace(/\$url\b/g, 'location'), true);
+ },
+ get breakStops() {
+ var def = "\\/\\?&#;\\s\\x00}<>"; // we stop on URL, JS and HTML delimiters
+ var bs = {
+ nq: new RegExp("[" + def + "]")
+ };
+ Array.forEach("'\"`", // special treatment for quotes
+ function(c) {
+ bs[c] = new RegExp("[" + def + c + "]");
+ }
+ );
+ delete this.breakStops;
+ return (this.breakStops = bs);
+ },
+
+ collapseChars: (s) => s.replace(/\;+/g, ';').replace(/\/{4,}/g, '////')
+ .replace(/\s+/g, (s) => /\n/g.test(s) ? '\n' : ' '),
+
+ _reduceBackslashes: (bs) => bs.length % 2 ? "\\" : "",
+
+ reduceQuotes: function(s) {
+ if (s[0] == '/') {
+ // reduce common leading path fragment resembling a regular expression or a comment
+ s = s.replace(/^\/[^\/\n\r]+\//, '_RX_').replace(/^\/\/[^\r\n]*/, '//_COMMENT_');
+ }
+
+ if (/\/\*/.test(s) || // C-style comments, would make everything really tricky
+ /\w\s*(\/\/[\s\S]*)?\[[\s\S]*\w[\s\S]*\]/.test(s)) { // property accessors, risky
+ return s;
+ }
+
+ if (/['"\/]/.test(s)) {
+
+ // drop noisy backslashes
+ s = s.replace(/\\{2,}/g, this._reduceBackslashes);
+
+ // drop escaped quotes
+ s = s.replace(/\\["'\/]/g, " EQ ");
+ var expr;
+ for (;;) {
+ expr = s.replace(/(^[^'"\/]*[;,\+\-=\(\[]\s*)\/[^\/]+\//g, "$1 _RX_ ")
+ .replace(/(^[^'"\/]*)(["']).*?\2/g, "$1 _QS_ ");
+ if (expr == s) break;
+ s = expr;
+ }
+ }
+
+ // remove c++ style comments
+ return s.replace(/^([^'"`\\]*?)\/\/[^\r\n]*/g, "$1//_COMMENT_");
+ },
+
+ reduceURLs: function(s) {
+ // nested URLs with protocol are parsed as C++ style comments, and since
+ // they're potentially very expensive, we preemptively remove them if possible
+ while (/^[^'"]*?:\/\//.test(s)) {
+ s = s.replace(/:\/\/[^*\s]*/, ':');
+ }
+ s = s.replace(/:\/\/[^'"*\n]*/g, ':');
+
+ return (/\bhttps?:$/.test(s) && !/\bh\W*t\W*t\W*p\W*s?.*=/.test(s)) ?
+ s.replace(/\b(?:[\w.]+=)?https?:$/, '') :
+ s;
+ },
+
+ reduceJSON(s) {
+ const REPL = 'J';
+ const toStringRx = /^function\s*toString\(\)\s*{\s*\[native code\]\s*\}$/;
+
+ // optimistic case first, one big JSON block
+ let m = s.match(/{[^]+}|\[[^]*{[^]+}[^]*\]/);
+ if (!m) return s;
+
+ // semicolon-separated JSON chunks, like on syndication.twitter.com
+ if (/}\s*;\s*{/.test(s)) s = s.split(";").map(chunk => this.reduceJSON(chunk)).join(";");
+
+ let [expr] = m;
+ try {
+ if (toStringRx.test(JSON.parse(expr).toString)) {
+ this.log("Reducing big JSON " + expr);
+ return this.reduceJSON(s.replace(expr, REPL));
+ }
+ } catch (e) {}
+ let iterations = 0;
+ for (;;) {
+ let prev = s;
+ let start = s.indexOf("{");
+ let end = s.lastIndexOf("}");
+ let prevExpr = "";
+ while (start > -1 && end - start > 1) {
+ expr = s.substring(start, end + 1);
+ if (expr === prevExpr) break;
+ end = s.lastIndexOf("}", end - 1);
+ if (end === -1) {
+ start = s.indexOf("{", start + 1);
+ end = s.lastIndexOf("}");
+ }
+ try {
+ if (!toStringRx.test(JSON.parse(expr).toString))
+ continue;
+
+ this.log("Reducing JSON " + expr);
+ s = s.replace(expr, REPL);
+ break;
+ } catch (e) {}
+
+ if (/\btoString\b[\s\S]*:/.test(expr)) {
+ continue;
+ }
+
+ let qred = this.reduceQuotes(expr);
+ if (/\{(?:\s*(?:(?:\w+:)+\w+)+;\s*)+\}/.test(qred)) {
+ this.log("Reducing pseudo-JSON " + expr);
+ s = s.replace(expr, REPL);
+ break;
+ }
+
+ if (!/[(=.]|[^:\s]\s*\[|:\s*(?:location|document|set(?:Timeout|Interval)|eval|open|show\w*Dialog|alert|confirm|prompt)\b|(?:\]|set)\s*:/.test(qred) &&
+ this.checkJSSyntax("JSON = " + qred) // no-assignment JSON fails with "invalid label"
+ ) {
+ this.log("Reducing slow JSON " + expr);
+ s = s.replace(expr, REPL);
+ break;
+ }
+ prevExpr = expr;
+ }
+
+ if (s === prev) break;
+ }
+
+ return s;
+ },
+
+ reduceXML: function reduceXML(s) {
+ var res;
+
+ for (let pos = s.indexOf("<"); pos !== -1; pos = s.indexOf("<", 1)) {
+
+ let head = s.substring(0, pos);
+ let tail = s.substring(pos);
+
+ let qnum = 0;
+ for (pos = -1;
+ (pos = head.indexOf('"', ++pos)) > -1;) {
+ if (pos === 0 || head[pos - 1] != '\\') qnum++;
+ }
+ if (qnum % 2) break; // odd quotes
+
+ let t = tail.replace(/^<(\??\s*\/?[a-zA-Z][\w:-]*)(?:[\s+]+[\w:-]+="[^"]*")*[\s+]*(\/?\??)>/, '<$1$2>');
+
+ (res || (res = [])).push(head);
+ s = t;
+ }
+ if (res) {
+ res.push(s);
+ s = res.join('');
+ }
+
+ return s;
+ },
+
+ _singleAssignmentRx: new RegExp(
+ "(?:\\b" + fuzzify('document') + "\\b[^]*\\.|\\s" + fuzzify('setter') + "\\b[^]*=)|/.*/[^]*(?:\\.(?:" +
+ "\\b" + fuzzify("onerror") + "\\b[^]*=|" +
+ +fuzzify('source|toString') + ")|\\[)|" + IC_EVENT_DOS_PATTERN
+ ),
+ _riskyAssignmentRx: new RegExp(
+ "\\b(?:" + fuzzify('location|innerHTML|outerHTML') + ")\\b[^]*="
+ ),
+ _nameRx: new RegExp(
+ "=[^]*\\b" + fuzzify('name') + "\\b|" +
+ fuzzify("hostname") + "[^]*=[^]*(?:\\b\\d|[\"'{}~^|<*/+-])"
+ ),
+ _evalAliasingRx: new RegExp(
+ "=[^]+\\[" + IC_EVAL_PATTERN + "\\W*\\]" // TODO: check if it can be coalesced into _maybeJSRx
+ ),
+
+ _maybeJSRx: new RegExp(
+ '(?:(?:\\[[^]+\\]|\\.\\D)(?:[^]*\\([^]*\\)|[^*]`[^]+`|[^=]*=[^=][^]*\\S)' +
+ // double function call
+ '|\\([^]*\\([^]*\\)' +
+ ')|(?:^|\\W)(?:' + IC_EVAL_PATTERN +
+ ')(?:\\W+[^]*|)[(`]|(?:[=(]|\\{[^]+:)[^]*(?:' + // calling eval-like functions directly or...
+ IC_EVAL_PATTERN + // ... assigning them to another function possibly called by the victim later
+ ')[^]*[\\n,;:|]|\\b(?:' +
+ fuzzify('setter|location|innerHTML|outerHTML') + // eval-like assignments
+ ')\\b[^]*=|' +
+ '.' + IC_COMMENT_PATTERN + "src" + IC_COMMENT_PATTERN + '=' +
+ IC_EVENT_DOS_PATTERN +
+ "|\\b" + fuzzify("onerror") + "\\b[^]*=" +
+ "|=[s\\\\[ux]?\d{2}" + // escape (unicode/ascii/octal)
+ "|\\b(?:toString|valueOf)\\b" + IC_COMMENT_PATTERN + "=[^]*(?:" + IC_EVAL_PATTERN + ")" +
+ "|(?:\\)|(?:[^\\w$]|^)[$a-zA-Z_\\u0ff-\\uffff][$\\w\\u0ff-\\uffff]*)" + IC_COMMENT_PATTERN + '=>' + // concise function definition
+ "|(?:[^\\w$]|^)" + IC_EVENT_PATTERN + IC_COMMENT_PATTERN + "="
+ ),
+
+ _riskyParensRx: new RegExp(
+ "(?:^|\\W)(?:(?:" + IC_EVAL_PATTERN + "|on\\w+)\\s*[(`]|" +
+ fuzzify("with") + "\\b[^]*\\(|" +
+ fuzzify("for") + "\\b[^]*\\([^]*[\\w$\\u0080-\\uffff]+[^]*\\b(?:" +
+ fuzzify("in|of") + ")\\b)"
+ ),
+
+ _dotRx: /\./g,
+ _removeDotsRx: /^openid\.[\w.-]+(?==)|(?:[?&#\/]|^)[\w.-]+(?=[\/\?&#]|$)|[\w\.]*\.(?:\b[A-Z]+|\w*\d|[a-z][$_])[\w.-]*|=[a-z.-]+\.(?:com|net|org|biz|info|xxx|[a-z]{2})(?:[;&/]|$)/g,
+ _removeDots: (p) => p.replace(InjectionChecker._dotRx, '|'),
+ _arrayAccessRx: /\s*\[\d+\]/g,
+ _riskyOperatorsRx: /[+-]{2}\s*(?:\/[*/][\s\S]+)?(?:\w+(?:\/[*/][\s\S]+)?[[.]|location)|(?:\]|\.\s*(?:\/[*/][\s\S]+)?\w+|location)\s*(?:\/[*/][\s\S]+)?([+-]{2}|[+*\/<>~-]+\s*(?:\/[*/][\s\S]+)?=)/, // inc/dec/self-modifying assignments on DOM props
+ _assignmentRx: /^(?:[^()="'\s]+=(?:[^(='"\[+]+|[?a-zA-Z_0-9;,&=/]+|[\d.|]+))$/,
+ _badRightHandRx: /=[\s\S]*(?:_QS_\b|[|.][\s\S]*source\b|<[\s\S]*\/[^>]*>)/,
+ _wikiParensRx: /^(?:[\w.|-]+\/)*\(*[\w\s-]+\([\w\s-]+\)[\w\s-]*\)*$/,
+ _neutralDotsRx: /(?:^|[\/;&#])[\w-]+\.[\w-]+[\?;\&#]/g,
+ _openIdRx: /^scope=(?:\w+\+)\w/, // OpenID authentication scope parameter, see http://forums.informaction.com/viewtopic.php?p=69851#p69851
+ _gmxRx: /\$\(clientName\)-\$\(dataCenter\)\.(\w+\.)+\w+/, // GMX webmail, see http://forums.informaction.com/viewtopic.php?p=69700#p69700
+
+ maybeJS(expr, mavoChecked = false) {
+ if (!mavoChecked && this.maybeMavo(expr)) return true;
+
+ if (/`[\s\S]*`/.test(expr) || // ES6 templates, extremely insidious!!!
+ this._evalAliasingRx.test(expr) ||
+ this._riskyOperatorsRx.test(expr) // this must be checked before removing dots...
+ ) return true;
+
+ expr = // dotted URL components can lead to false positives, let's remove them
+ expr.replace(this._removeDotsRx, this._removeDots)
+ .replace(this._arrayAccessRx, '_ARRAY_ACCESS_')
+ .replace(/<([\w:]+)>[^</(="'`]+<\/\1>/g, '<$1/>') // reduce XML text nodes
+ .replace(/<!--/g, '') // remove HTML comments preamble (see next line)
+ .replace(/(^(?:[^/]*[=;.+-])?)\s*[\[(]+/g, '$1') // remove leading parens and braces
+ .replace(this._openIdRx, '_OPENID_SCOPE_=XYZ')
+ .replace(/^[^=]*OPENid\.(\w+)=/gi, "OPENid_\1")
+ .replace(this._gmxRx, '_GMX_-_GMX_');
+
+ if (expr.indexOf(")") !== -1) expr += ")"; // account for externally balanced parens
+ if (this._assignmentRx.test(expr) && !this._badRightHandRx.test(expr)) // commonest case, single assignment or simple chained assignments, no break
+ return this._singleAssignmentRx.test(expr) || this._riskyAssignmentRx.test(expr) && this._nameRx.test(expr);
+
+ return this._riskyParensRx.test(expr) ||
+ this._maybeJSRx.test(expr.replace(this._neutralDotsRx, '')) &&
+ !this._wikiParensRx.test(expr);
+
+ },
+
+ checkNonTrivialJSSyntax: function(expr) {
+ return this.maybeJS(this.reduceQuotes(expr)) && this.checkJSSyntax(expr);
+ },
+
+
+ wantsExpression: (s) => /(?:^[+-]|[!%&(,*/:;<=>?\[^|]|[^-]-|[^+]\+)\s*$/.test(s),
+
+ stripLiteralsAndComments: function(s) {
+ "use strict";
+
+ const MODE_NORMAL = 0;
+ const MODE_REGEX = 1;
+ const MODE_SINGLEQUOTE = 2;
+ const MODE_DOUBLEQUOTE = 3;
+ const MODE_BLOCKCOMMENT = 4;
+ const MODE_LINECOMMENT = 6;
+ const MODE_INTERPOLATION = 7;
+
+ let mode = MODE_NORMAL;
+ let escape = false;
+ let res = [];
+
+ function handleQuotes(c, q, type) {
+ if (escape) {
+ escape = false;
+ } else if (c == '\\') {
+ escape = true;
+ } else if (c === q) {
+ res.push(type);
+ mode = MODE_NORMAL;
+ }
+ }
+ for (let j = 0, l = s.length; j < l; j++) {
+
+ switch (mode) {
+ case MODE_REGEX:
+ handleQuotes(s[j], '/', "_REGEXP_");
+ break;
+ case MODE_SINGLEQUOTE:
+ handleQuotes(s[j], "'", "_QS_");
+ break;
+ case MODE_DOUBLEQUOTE:
+ handleQuotes(s[j], '"', "_DQS_");
+ break;
+ case MODE_INTERPOLATION:
+ handleQuotes(s[j], '`', "``");
+ break;
+ case MODE_BLOCKCOMMENT:
+ if (s[j] === '/' && s[j - 1] === '*') {
+ res.push("/**/");
+ mode = MODE_NORMAL;
+ }
+ break;
+ case MODE_LINECOMMENT:
+ if (s[j] === '\n') {
+ res.push("//\n");
+ mode = MODE_NORMAL;
+ }
+ break;
+ default:
+ switch (s[j]) {
+ case '"':
+ mode = MODE_DOUBLEQUOTE;
+ break;
+ case "'":
+ mode = MODE_SINGLEQUOTE;
+ break;
+ case "`":
+ mode = MODE_INTERPOLATION;
+ break;
+ case '/':
+ switch (s[j + 1]) {
+ case '*':
+ mode = MODE_BLOCKCOMMENT;
+ j += 2;
+ break;
+ case '/':
+ mode = MODE_LINECOMMENT;
+ break;
+ default:
+ let r = res.join('');
+ res = [r];
+ if (this.wantsExpression(r)) mode = MODE_REGEX;
+ else res.push('/'); // after a self-contained expression: division operator
+ }
+ break;
+ default:
+ res.push(s[j]);
+ }
+
+ }
+ }
+ return res.join('');
+ },
+
+ checkLastFunction: function() {
+ var fn = this.syntax.lastFunction;
+ if (!fn) return false;
+ var m = fn.toSource().match(/\{([\s\S]*)\}/);
+ if (!m) return false;
+ var expr = this.stripLiteralsAndComments(m[1]);
+ return /=[\s\S]*cookie|\b(?:setter|document|location|(?:inn|out)erHTML|\.\W*src)[\s\S]*=|[\w$\u0080-\uffff\)\]]\s*[\[\(]/.test(expr) ||
+ this.maybeJS(expr);
+ },
+
+ _createInvalidRanges: function() {
+ function x(n) {
+ return '\\u' + ("0000" + n.toString(16)).slice(-4);
+ }
+
+ var ret = "";
+ var first = -1;
+ var last = -1;
+ var cur = 0x7e;
+ while (cur++ <= 0xffff) {
+ try {
+ eval("var _" + String.fromCharCode(cur) + "_=1");
+ } catch (e) {
+ if (!/illegal char/.test(e.message)) continue;
+ if (first == -1) {
+ first = last = cur;
+ ret += x(cur);
+ continue;
+ }
+ if (cur - last == 1) {
+ last = cur;
+ continue;
+ }
+
+ if (last != first) ret += "-" + x(last);
+ ret += x(cur);
+ last = first = cur;
+ }
+ }
+ return ret;
+ },
+
+ get invalidCharsRx() {
+ delete this.invalidCharsRx;
+ return this.invalidCharsRx = new RegExp("^[^\"'`/<>]*[" + this._createInvalidRanges() + "]");
+ },
+
+ checkJSBreak: function InjectionChecker_checkJSBreak(s) {
+ // Direct script injection breaking JS string literals or comments
+
+
+ // cleanup most urlencoded noise and reduce JSON/XML
+ s = ';' + this.reduceXML(this.reduceJSON(this.collapseChars(
+ s.replace(/\%\d+[a-z\(]\w*/gi, '§')
+ .replace(/[\r\n\u2028\u2029]+/g, "\n")
+ .replace(/[\x01-\x09\x0b-\x20]+/g, ' ')
+ )));
+
+ if (s.indexOf("*/") > 0 && /\*\/[\s\S]+\/\*/.test(s)) { // possible scrambled multi-point with comment balancing
+ s += ';' + s.match(/\*\/[\s\S]+/);
+ }
+
+ if (!this.maybeJS(s)) return false;
+
+ const MAX_TIME = 8000,
+ MAX_LOOPS = 1200;
+
+ const logEnabled = this.logEnabled;
+
+ const
+ invalidCharsRx = /[\u007f-\uffff]/.test(s) && this.invalidCharsRx,
+ dangerRx = /\(|(?:^|[+-]{2}|[+*/<>~-]+\\s*=)|`[\s\S]*`|\[[^\]]+\]|(?:setter|location|(?:inn|out)erHTML|cookie|on\w{3,}|\.\D)[^&]*=[\s\S]*?(?:\/\/|[\w$\u0080-\uFFFF.[\]})'"-]+)/,
+ exprMatchRx = /^[\s\S]*?(?:[=\)]|`[\s\S]*`|[+-]{2}|[+*/<>~-]+\\s*=)/,
+ safeCgiRx = /^(?:(?:[\.\?\w\-\/&:§\[\]]+=[\w \-:\+%#,§\.]*(?:[&\|](?=[^&\|])|$)){2,}|\w+:\/\/\w[\w\-\.]*)/,
+ // r2l, chained query string parameters, protocol://domain
+ headRx = /^(?:[^'"\/\[\(]*[\]\)]|[^"'\/]*(?:§|[^&]&[\w\.]+=[^=]))/
+ // irrepairable syntax error, such as closed parens in the beginning
+ ;
+
+ const injectionFinderRx = /(['"`#;>:{}]|[/?=](?![?&=])|&(?![\w-.[\]&!-]*=)|\*\/)(?!\1)/g;
+ injectionFinderRx.lastIndex = 0;
+
+ const t = Date.now();
+ var iterations = 0;
+
+ for (let dangerPos = 0, m;
+ (m = injectionFinderRx.exec(s));) {
+
+ let startPos = injectionFinderRx.lastIndex;
+ let subj = s.substring(startPos);
+ if (startPos > dangerPos) {
+ dangerRx.lastIndex = startPos;
+ if (!dangerRx.exec(s)) {
+ this.log("Can't find any danger in " + s);
+ return false;
+ }
+ dangerPos = dangerRx.lastIndex;
+ }
+
+ let breakSeq = m[1];
+ let quote = breakSeq in this.breakStops ? breakSeq : '';
+
+ if (!this.maybeJS(quote ? quote + subj : subj)) {
+ this.log("Fast escape on " + subj, t, iterations);
+ return false;
+ }
+
+ let script = this.reduceURLs(subj);
+
+ if (script.length < subj.length) {
+ if (!this.maybeJS(script)) {
+ this.log("Skipping to first nested URL in " + subj, t, iterations);
+ injectionFinderRx.lastIndex += subj.indexOf("://") + 1;
+ continue;
+ }
+ subj = script;
+ script = this.reduceURLs(subj.substring(0, dangerPos - startPos));
+ } else {
+ script = subj.substring(0, dangerPos - startPos);
+ }
+
+ let expr = subj.match(exprMatchRx);
+
+ if (expr) {
+ expr = expr[0];
+ if (expr.length < script.length) {
+ expr = script;
+ }
+ } else {
+ expr = script;
+ }
+
+ // quickly skip (mis)leading innocuous CGI patterns
+ if ((m = subj.match(safeCgiRx))) {
+
+ this.log("Skipping CGI pattern in " + subj);
+
+ injectionFinderRx.lastIndex += m[0].length - 1;
+ continue;
+ }
+
+ let bs = this.breakStops[quote || 'nq']
+
+ for (let len = expr.length, moved = false, hunt = !!expr, lastExpr = ''; hunt;) {
+
+ if (Date.now() - t > MAX_TIME) {
+ this.log("Too long execution time! Assuming DOS... " + (Date.now() - t), t, iterations);
+ return true;
+ }
+
+ hunt = expr.length < subj.length;
+
+ if (moved) {
+ moved = false;
+ } else if (hunt) {
+ let pos = subj.substring(len).search(bs);
+ if (pos < 0) {
+ expr = subj;
+ hunt = false;
+ } else {
+ len += pos;
+ if (quote && subj[len] === quote) {
+ len++;
+ } else if (subj[len - 1] === '<') {
+ // invalid JS, and maybe in the middle of XML block
+ len++;
+ continue;
+ }
+ expr = subj.substring(0, len);
+ if (pos === 0) len++;
+ }
+ }
+
+ if (lastExpr === expr) {
+ lastExpr = '';
+ continue;
+ }
+
+ lastExpr = expr;
+
+ if (invalidCharsRx && invalidCharsRx.test(expr)) {
+ this.log("Quick skipping invalid chars");
+ break;
+ }
+
+
+
+ if (quote) {
+ if (this.checkNonTrivialJSSyntax(expr)) {
+ this.log("Non-trivial JS inside quoted string detected", t, iterations);
+ return true;
+ }
+ script = this.syntax.unquote(quote + expr, quote);
+ if (script && this.maybeJS(script) &&
+ (this.checkNonTrivialJSSyntax(script) ||
+ /'./.test(script) && this.checkNonTrivialJSSyntax("''" + script + "'") ||
+ /"./.test(script) && this.checkNonTrivialJSSyntax('""' + script + '"')
+ ) && this.checkLastFunction()
+ ) {
+ this.log("JS quote Break Injection detected", t, iterations);
+ return true;
+ }
+ script = quote + quote + expr + quote;
+ } else {
+ script = expr;
+ }
+
+ if (headRx.test(script.split("//")[0])) {
+ let balanced = script.replace(/^[^"'{}(]*\)/, 'P ');
+ if (balanced !== script && balanced.indexOf('(') > -1) {
+ script = balanced + ")";
+ } else {
+ this.log("SKIP (head syntax) " + script, t, iterations);
+ break; // unrepairable syntax error in the head, move left cursor forward
+ }
+ }
+
+ if (this.maybeJS(this.reduceQuotes(script))) {
+
+ if (this.checkJSSyntax(script) && this.checkLastFunction()) {
+ this.log("JS Break Injection detected", t, iterations);
+ return true;
+ }
+
+ if (this.checkTemplates(script)) {
+ this.log("JS template expression injection detected", t, iterations);
+ return true;
+ }
+
+ if (++iterations > MAX_LOOPS) {
+ this.log("Too many syntax checks! Assuming DOS... " + s, t, iterations);
+ return true;
+ }
+ if (this.syntax.lastError) { // could be null if we're here thanks to checkLastFunction()
+ let errmsg = this.syntax.lastError.message;
+ if (logEnabled) this.log(errmsg + " --- " + this.syntax.lastScript + " --- ", t, iterations);
+ if (!quote) {
+ if (errmsg.indexOf("left-hand") !== -1) {
+ let m = subj.match(/^([^\]\(\\'"=\?]+?)[\w$\u0080-\uffff\s]+[=\?]/);
+ if (m) {
+ injectionFinderRx.lastIndex += m[1].length - 1;
+ }
+ break;
+ } else if (errmsg.indexOf("unterminated string literal") !== -1) {
+ let quotePos = subj.substring(len).search(/["']/);
+ if (quotePos > -1) {
+ expr = subj.substring(0, len += ++quotePos);
+ moved = true;
+ } else break;
+ } else if (errmsg.indexOf("syntax error") !== -1) {
+ let dblSlashPos = subj.indexOf("//");
+ if (dblSlashPos > -1) {
+ let pos = subj.search(/['"\n\\\(]|\/\*/);
+ if (pos < 0 || pos > dblSlashPos)
+ break;
+ }
+ if (/^([\w\[\]]*=)?\w*&[\w\[\]]*=/.test(subj)) { // CGI param concatenation
+ break;
+ }
+ }
+ } else if (errmsg.indexOf("left-hand") !== -1) break;
+
+ if (/invalid .*\bflag\b|missing ; before statement|invalid label|illegal character|identifier starts immediately/.test(errmsg)) {
+ if (errmsg.indexOf("illegal character") === -1 && /#\d*\s*$/.test(script)) { // sharp vars exceptional behavior
+ if (!quote) break;
+ // let's retry without quotes
+ quote = lastExpr = '';
+ hunt = moved = true;
+ } else break;
+ } else if ((m = errmsg.match(/\b(?:property id\b|missing ([:\]\)\}]) )/))) {
+ let char = m[1] || '}';
+ let newLen = subj.indexOf(char, len);
+ let nextParamPos = subj.substring(len).search(/[^&]&(?!&)/)
+ if (newLen !== -1 && (nextParamPos === -1 || newLen <= len + nextParamPos)) {
+ this.log("Extending to next " + char);
+ expr = subj.substring(0, len = ++newLen);
+ moved = char !== ':';
+ } else if (char !== ':') {
+ let lastChar = expr[expr.length - 1];
+ if (lastChar === char && (len > subj.length || lastChar != subj[len - 1])) break;
+ expr += char;
+ moved = hunt = true;
+ len++;
+ this.log("Balancing " + char, t, iterations);
+ } else {
+ break;
+ }
+ } else if (/finally without try/.test(errmsg)) {
+ expr = "try{" + expr;
+ hunt = moved = true;
+ }
+ }
+ }
+ }
+ }
+ this.log(s, t, iterations);
+ return false;
+ },
+
+
+ checkJS: function(s, unescapedUni) {
+ this.log(s);
+
+ if (/\?name\b[\s\S]*:|[^&?]\bname\b/.test(s)) {
+ this.nameAssignment = true;
+ }
+
+ var hasUnicodeEscapes = !unescapedUni && /\\u(?:[0-9a-f]{4}|\{[0-9a-f]+\})/i.test(s);
+ if (hasUnicodeEscapes && /\\u(?:\{0*|00)[0-7][0-9a-f]/i.test(s)) {
+ this.escalate("Unicode-escaped lower ASCII");
+ return true;
+ }
+
+ if (/\\x[0-9a-f]{2}[\s\S]*['"]/i.test(s)) {
+ this.escalate("Obfuscated string literal");
+ return true;
+ }
+
+ if (/`[\s\S]*\$\{[\s\S]+[=(][\s\S]+\}[\s\S]*`/.test(s)) {
+ this.escalate("ES6 string interpolation");
+ return true;
+ }
+
+ this.syntax.lastFunction = null;
+ let ret = this.checkAttributes(s) ||
+ (/[\\\(]|=[^=]/.test(s) || this._riskyOperatorsRx.test(s)) && this.checkJSBreak(s) || // MAIN
+ hasUnicodeEscapes && this.checkJS(this.unescapeJS(s), true); // optional unescaped recursion
+ if (ret) {
+ let msg = "JavaScript Injection in " + s;
+ if (this.syntax.lastFunction) {
+ msg += "\n" + this.syntax.lastFunction.toSource();
+ }
+ this.escalate(msg);
+ }
+ return ret;
+ },
+
+ unescapeJS: function(s) {
+ return s.replace(/\\u([0-9a-f]{4})/gi, function(s, c) {
+ return String.fromCharCode(parseInt(c, 16));
+ });
+ },
+ unescapeJSLiteral: function(s) {
+ return s.replace(/\\x([0-9a-f]{2})/gi, function(s, c) {
+ return String.fromCharCode(parseInt(c, 16));
+ });
+ },
+
+ unescapeCSS: function(s) {
+ // see http://www.w3.org/TR/CSS21/syndata.html#characters
+ return s.replace(/\\([\da-f]{0,6})\s?/gi, function($0, $1) {
+ try {
+ return String.fromCharCode(parseInt($1, 16));
+ } catch (e) {
+ return "";
+ }
+ });
+ },
+
+ reduceDashPlus: function(s) {
+ // http://forums.mozillazine.org/viewtopic.php?p=5592865#p5592865
+ return s.replace(/\-+/g, "-")
+ .replace(/\++/g, "+")
+ .replace(/\s+/g, ' ')
+ .replace(/(?: \-)+/g, ' -')
+ .replace(/(?:\+\-)+/g, '+-');
+ },
+
+ _rxCheck: function(checker, s) {
+ var rx = this[checker + "Checker"];
+ var ret = rx.exec(s);
+ if (ret) {
+ this.escalate(checker + " injection:\n" + ret + "\nmatches " + rx.source);
+ return true;
+ }
+ return false;
+ },
+
+ AttributesChecker: new RegExp(
+ "(?:\\W|^)(?:javascript:(?:[^]+[=\\\\\\(`\\[\\.<]|[^]*(?:\\bname\\b|\\\\[ux]\\d))|" +
+ "data:(?:(?:[a-z]\\w+/\\w[\\w+-]+\\w)?[;,]|[^]*;[^]*\\b(?:base64|charset=)|[^]*,[^]*<[^]*\\w[^]*>))|@" +
+ ("import\\W*(?:\\/\\*[^]*)?(?:[\"']|url[^]*\\()" +
+ "|-moz-binding[^]*:[^]*url[^]*\\(|\\{\\{[^]+\\}\\}")
+ .replace(/[a-rt-z\-]/g, "\\W*$&"),
+ "i"),
+ checkAttributes: function(s) {
+ s = this.reduceDashPlus(s);
+ if (this._rxCheck("Attributes", s)) return true;
+ if (/\\/.test(s) && this._rxCheck("Attributes", this.unescapeCSS(s))) return true;
+ let dataPos = s.search(/data:\S*\s/i);
+ if (dataPos !== -1) {
+ let data = this.urlUnescape(s.substring(dataPos).replace(/\s/g, ''));
+ if (this.checkHTML(data) || this.checkAttributes(data)) return true;
+ }
+ return false;
+ },
+
+ GlobalsChecker: /https?:\/\/[\S\s]+["'\s\0](?:id|class|data-\w+)[\s\0]*=[\s\0]*("')?\w{3,}(?:[\s\0]|\1|$)|(?:id|class|data-\w+)[\s\0]*=[\s\0]*("')?\w{3,}(?:[\s\0]|\1)[\s\S]*["'\s\0]href[\s\0]*=[\s\0]*(?:"')?https?:\/\//i,
+ HTMLChecker: new RegExp("<[^\\w<>]*(?:[^<>\"'\\s]*:)?[^\\w<>]*(?:" + // take in account quirks and namespaces
+ fuzzify("script|form|style|svg|marquee|(?:link|object|embed|applet|param|i?frame|base|body|meta|ima?ge?|video|audio|bindings|set|isindex|animate|template") +
+ ")[^>\\w])|['\"\\s\\0/](?:formaction|style|background|src|lowsrc|ping|innerhtml|data-bind|(?:data-)?mv-(?:\\w+[\\w-]*)|" + IC_EVENT_PATTERN +
+ ")[\\s\\0]*=|<%[^]+[=(][^]+%>", "i"),
+
+ checkHTML(s) {
+ let links = s.match(/\b(?:href|src|base|(?:form)?action|\w+-\w+)\s*=\s*(?:(["'])[\s\S]*?\1|(?:[^'">][^>\s]*)?[:?\/#][^>\s]*)/ig);
+ if (links) {
+ for (let l of links) {
+ l = l.replace(/[^=]*=\s*/i, '').replace(/[\u0000-\u001f]/g, '');
+ l = /^["']/.test(l) ? l.replace(/^(['"])([^]*?)\1[^]*/g, '$2') : l.replace(/[\s>][^]*/, '');
+
+ if (/^(?:javascript|data):|\[[^]+\]/i.test(l) || /[<'"(]/.test(unescape(l)) && this.checkUrl(l)) return true;
+ }
+ }
+ return this._rxCheck("HTML", s) || this._rxCheck("Globals", s);
+ },
+
+ checkNoscript: function(s) {
+ this.log(s);
+ return s.indexOf("\x1b(J") !== -1 && this.checkNoscript(s.replace(/\x1b\(J/g, '')) || // ignored in iso-2022-jp
+ s.indexOf("\x7e\x0a") !== -1 && this.checkNoscript(s.replace(/\x7e\x0a/g, '')) || // ignored in hz-gb-2312
+ this.checkHTML(s) || this.checkSQLI(s) || this.checkHeaders(s);
+ },
+
+ HeadersChecker: /[\r\n]\s*(?:content-(?:type|encoding))\s*:/i,
+ checkHeaders: function(s) {
+ return this._rxCheck("Headers", s);
+ },
+ SQLIChecker: /(?:(?:(?:\b|[^a-z])union[^a-z]|\()[\w\W]*(?:\b|[^a-z])select[^a-z]|(?:updatexml|extractvalue)(?:\b|[^a-z])[\w\W]*\()[\w\W]+(?:(?:0x|x')[0-9a-f]{16}|(?:0b|b')[01]{64}|\(|\|\||\+)/i,
+ checkSQLI: function(s) {
+ return this._rxCheck("SQLI", s);
+ },
+
+ base64: false,
+ base64tested: [],
+ get base64Decoder() {
+ return Base64;
+ }, // exposed here just for debugging purposes
+
+
+ checkBase64: function(url) {
+ this.base64 = false;
+
+ const MAX_TIME = 8000;
+ const DOS_MSG = "Too long execution time, assuming DOS in Base64 checks";
+
+ this.log(url);
+
+
+ var parts = url.split("#"); // check hash
+ if (parts.length > 1 && this.checkBase64FragEx(unescape(parts[1])))
+ return true;
+
+ parts = parts[0].split(/[&;]/); // check query string
+ if (parts.length > 0 && parts.some(function(p) {
+ var pos = p.indexOf("=");
+ if (pos > -1) p = p.substring(pos + 1);
+ return this.checkBase64FragEx(unescape(p));
+ }, this))
+ return true;
+
+ url = parts[0];
+ parts = Base64.purify(url).split("/");
+ if (parts.length > 255) {
+ this.log("More than 255 base64 slash chunks, assuming DOS");
+ return true;
+ }
+
+
+ var t = Date.now();
+ if (parts.some(function(p) {
+ if (Date.now() - t > MAX_TIME) {
+ this.log(DOS_MSG);
+ return true;
+ }
+ return this.checkBase64Frag(Base64.purify(Base64.alt(p)));
+ }, this))
+ return true;
+
+
+ var uparts = Base64.purify(unescape(url)).split("/");
+
+ t = Date.now();
+ while (parts.length) {
+ if (Date.now() - t > MAX_TIME) {
+ this.log(DOS_MSG);
+ return true;
+ }
+ if (this.checkBase64Frag(parts.join("/")) ||
+ this.checkBase64Frag(uparts.join("/")))
+ return true;
+
+ parts.shift();
+ uparts.shift();
+ }
+
+ return false;
+ },
+
+
+ checkBase64Frag: function(f) {
+ if (this.base64tested.indexOf(f) < 0) {
+ this.base64tested.push(f);
+ try {
+ var s = Base64.decode(f);
+ if (s && s.replace(/[^\w\(\)]/g, '').length > 7 &&
+ (this.checkHTML(s) ||
+ this.checkAttributes(s))
+ // this.checkJS(s) // -- alternate, whose usefulness is doubious but which easily leads to DOS
+ ) {
+ this.log("Detected BASE64 encoded injection: " + f + " --- (" + s + ")");
+ return this.base64 = true;
+ }
+ } catch (e) {}
+ }
+ return false;
+ },
+
+ checkBase64FragEx: function(f) {
+ return this.checkBase64Frag(Base64.purify(f)) || this.checkBase64Frag(Base64.purify(Base64.alt(f)));
+ },
+
+
+ checkUrl(url, skipRx = null) {
+ if (skipRx) url = url.replace(skipRx, '');
+ return this.checkRecursive(url
+ // assume protocol and host are safe, but keep the leading double slash to keep comments in account
+ .replace(/^[a-z]+:\/\/.*?(?=\/|$)/, "//")
+ // Remove outer parenses from ASP.NET cookieless session's AppPathModifier
+ .replace(/\/\((S\(\w{24}\))\)\//, '/$1/')
+ );
+ },
+
+ checkPost(formData, skipParams = null) {
+ let keys = Object.keys(formData);
+ if (Array.isArray(skipParams)) keys = keys.filter(k => !skipParams.includes(k))
+ for (let key of keys) {
+ let chunk = `${key}=${formData[key].join(`;`)}`;
+ if (this.checkRecursive(chunk, 2, true)) {
+ return chunk;
+ }
+ }
+ return null;
+ },
+
+ checkRecursive(s, depth = 3, isPost = false) {
+ this.reset();
+ this.isPost = isPost;
+
+
+ if (ASPIdiocy.affects(s)) {
+ if (this.checkRecursive(ASPIdiocy.process(s), depth, isPost))
+ return true;
+ } else if (ASPIdiocy.hasBadPercents(s) && this.checkRecursive(ASPIdiocy.removeBadPercents(s), depth, isPost)) {
+ return true;
+ }
+ if (FlashIdiocy.affects(s)) {
+ let purged = FlashIdiocy.purgeBadEncodings(s);
+ if (purged !== s && this.checkRecursive(purged, depth, isPost))
+ return true;
+ let decoded = FlashIdiocy.platformDecode(purged);
+ if (decoded !== purged && this.checkRecursive(decoded, depth, isPost))
+ return true;
+ }
+
+ if (s.indexOf("coalesced:") !== 0) {
+ let coalesced = ASPIdiocy.coalesceQuery(s);
+ if (coalesced !== s && this.checkRecursive("coalesced:" + coalesced, depth, isPost))
+ return true;
+ }
+
+ if (isPost) {
+ s = this.formUnescape(s);
+ if (this.checkBase64Frag(Base64.purify(s))) return true;
+
+ if (s.indexOf("<") > -1) {
+ // remove XML-embedded Base64 binary data
+ s = s.replace(/<((?:\w+:)?\w+)>[0-9a-zA-Z+\/]+=*<\/\1>/g, '');
+ }
+
+ s = "#" + s;
+ } else {
+ if (this.checkBase64(s.replace(/^\/{1,3}/, ''))) return true;
+ }
+
+ if (isPost) s = "#" + s; // allows the string to be JS-checked as a whole
+ return this._checkRecursive(s, depth);
+ },
+
+ _checkRecursive: function(s, depth) {
+
+ if (this.checkHTML(s) || this.checkJS(s) || this.checkSQLI(s) || this.checkHeaders(s))
+ return true;
+
+ if (s.indexOf("&") !== -1) {
+ let unent = Entities.convertAll(s);
+ if (unent !== s && this._checkRecursive(unent, depth)) return true;
+ }
+
+ if (--depth <= 0)
+ return false;
+
+ if (s.indexOf('+') !== -1 && this._checkRecursive(this.formUnescape(s), depth))
+ return true;
+
+ var unescaped = this.urlUnescape(s);
+ let badUTF8 = this.utf8EscapeError;
+
+ if (this._checkOverDecoding(s, unescaped))
+ return true;
+
+ if (/[\u0000-\u001f]|&#/.test(unescaped)) {
+ let unent = Entities.convertAll(unescaped.replace(/[\u0000-\u001f]+/g, ''));
+ if (unescaped != unent && this._checkRecursive(unent, depth)) {
+ this.log("Trash-stripped nested URL match!");
+ return true;
+ }
+ }
+
+ if (/\\x[0-9a-f]/i.test(unescaped)) {
+ let literal = this.unescapeJSLiteral(unescaped);
+ if (unescaped !== literal && this._checkRecursive(literal, depth)) {
+ this.log("Escaped literal match!");
+ return true;
+ }
+ }
+
+ if (unescaped.indexOf("\x1b(J") !== -1 && this._checkRecursive(unescaped.replace(/\x1b\(J/g, ''), depth) || // ignored in iso-2022-jp
+ unescaped.indexOf("\x7e\x0a") !== -1 && this._checkRecursive(unescaped.replace(/\x7e\x0a/g, '')) // ignored in hz-gb-2312
+ ) {
+ return true;
+ }
+
+ if (badUTF8) {
+ try {
+ let legacyEscaped = unescape(unescaped);
+ if (legacyEscaped !== unescaped && this._checkRecursive(unescape(unescaped))) return true;
+ } catch (e) {}
+ }
+
+ if (unescaped !== s && this._checkRecursive(unescaped, depth)) {
+ return true;
+ }
+
+ s = this.ebayUnescape(unescaped);
+ if (s != unescaped && this._checkRecursive(s, depth))
+ return true;
+
+ return false;
+ },
+
+ _checkOverDecoding: function(s, unescaped) {
+ if (/%[8-9a-f]/i.test(s)) {
+ const rx = /[<'"]/g;
+ var m1 = unescape(this.utf8OverDecode(s, false)).match(rx);
+ if (m1) {
+ unescaped = unescaped || this.urlUnescape(s);
+ var m0 = unescaped.match(rx);
+ if (!m0 || m0.length < m1.length) {
+ this.log("Potential utf8_decode() exploit!");
+ return true;
+ }
+ }
+ }
+ return false;
+ },
+
+ utf8OverDecode: function(url, strict) {
+ return url.replace(strict ?
+ /%(?:f0%80%80|e0%80|c0)%[8-b][0-f]/gi :
+ /%(?:f[a-f0-9](?:%[0-9a-f]0){2}|e0%[4-9a-f]0|c[01])%[a-f0-9]{2}/gi,
+ function(m) {
+ var hex = m.replace(/%/g, '');
+ if (strict) {
+ for (var j = 2; j < hex.length; j += 2) {
+ if ((parseInt(hex.substring(j, j + 2), 16) & 0xc0) != 0x80) return m;
+ }
+ }
+ switch (hex.length) {
+ case 8:
+ hex = hex.substring(2);
+ case 6:
+ c = (parseInt(hex.substring(0, 2), 16) & 0x3f) << 12 |
+ (parseInt(hex.substring(2, 4), 16) & 0x3f) << 6 |
+ parseInt(hex.substring(4, 6), 16) & 0x3f;
+ break;
+ default:
+ c = (parseInt(hex.substring(0, 2), 16) & 0x3f) << 6 |
+ parseInt(hex.substring(2, 4), 16) & 0x3f;
+ }
+ return encodeURIComponent(String.fromCharCode(c & 0x3f));
+ }
+ );
+ },
+
+ utf8EscapeError: true,
+ urlUnescape: function(url, brutal) {
+ var od = this.utf8OverDecode(url, !brutal);
+ this.utf8EscapeError = false;
+ try {
+ return decodeURIComponent(od);
+ } catch (warn) {
+ this.utf8EscapeError = true;
+ if (url != od) url += " (" + od + ")";
+ this.log("Problem decoding " + url + ", maybe not an UTF-8 encoding? " + warn.message);
+ return od;
+ }
+ },
+
+ formUnescape: function(s, brutal) {
+ return this.urlUnescape(s.replace(/\+/g, ' '), brutal);
+ },
+
+ ebayUnescape: function(url) {
+ return url.replace(/Q([\da-fA-F]{2})/g, function(s, c) {
+ return String.fromCharCode(parseInt(c, 16));
+ });
+ },
+
+ checkWindowName(window, url) {
+ var originalAttempt = window.name;
+ try {
+ if (/^https?:\/\/(?:[^/]*\.)?\byimg\.com\/rq\/darla\//.test(url)) {
+ window.name = "DARLA_JUNK";
+ return;
+ }
+
+ if (/\s*{[\s\S]+}\s*/.test(originalAttempt)) {
+ try {
+ JSON.parse(originalAttempt); // fast track for crazy JSON in name like on NYT
+ return;
+ } catch (e) {}
+ }
+
+ if (/[%=\(\\<]/.test(originalAttempt) && InjectionChecker.checkUrl(originalAttempt)) {
+ window.name = originalAttempt.replace(/[%=\(\\<]/g, " ");
+ }
+
+ if (originalAttempt.length > 11) {
+ try {
+ if ((originalAttempt.length % 4 === 0)) {
+ var bin = window.atob(window.name);
+ if (/[%=\(\\]/.test(bin) && InjectionChecker.checkUrl(bin)) {
+ window.name = "BASE_64_XSS";
+ }
+ }
+ } catch (e) {}
+ }
+ } finally {
+ if (originalAttempt != window.name) {
+ log('[NoScript XSS]: sanitized window.name, "' + originalAttempt + '"\nto\n"' + window.name + '"\nURL: ' + url);
+ log(url + "\n" + window.location.href);
+ }
+ }
+ },
+
+ };
+ // InjectionChecker.logEnabled = true;
+ return InjectionChecker;
+})();
diff --git a/src/xss/XSS.js b/src/xss/XSS.js
new file mode 100644
index 0000000..94e33fa
--- /dev/null
+++ b/src/xss/XSS.js
@@ -0,0 +1,246 @@
+'use strict';
+
+var XSS = (() => {
+
+ const ABORT = {cancel: true}, ALLOW = {};
+
+ let promptsMap = new Map();
+
+ async function getUserResponse(xssReq) {
+ let {originKey} = xssReq;
+ await promptsMap.get(originKey);
+ // promptsMap.delete(originKey);
+ switch (await XSS.getUserChoice(originKey)) {
+ case "allow":
+ return ALLOW;
+ case "block":
+ log("Blocking request from %s to %s by previous XSS prompt user choice",
+ xssReq.srcUrl, xssReq.destUrl);
+ return ABORT;
+ }
+ return null;
+ }
+
+ async function requestListener(request) {
+
+ if (ns.isEnforced(request.tabId)) {
+ let {policy} = ns;
+ let {type} = request;
+ if (type !== "main_frame") {
+ if (type === "sub_frame") type = "frame";
+ if (!policy.can(request.url, type, request.originUrl)) {
+ return ALLOW; // it will be blocked by RequestGuard
+ }
+ }
+ }
+ let xssReq = XSS.parseRequest(request);
+ if (!xssReq) return null;
+ let userResponse = await getUserResponse(xssReq);
+ if (userResponse) return userResponse;
+
+ let data;
+ let reasons;
+ try {
+ reasons = await XSS.maybe(xssReq);
+ if (!reasons) return ALLOW;
+
+ data = [];
+ } catch (e) {
+ error(e, "XSS filter processing %o", xssReq);
+ reasons = { urlInjection: true };
+ data = [e.toString()];
+ }
+
+
+
+ let prompting = (async () => {
+ userResponse = await getUserResponse(xssReq);
+ if (userResponse) return userResponse;
+
+ let {srcOrigin, destOrigin, unescapedDest} = xssReq;
+ let block = !!(reasons.urlInjection || reasons.postInjection)
+
+ if (reasons.protectName) {
+ RequestUtil.executeOnStart(request, {
+ file: "/xss/sanitizeName.js",
+ });
+ if (!block) return ALLOW;
+ }
+ if (reasons.urlInjection) data.push(`(URL) ${unescapedDest}`);
+ if (reasons.postInjection) data.push(`(POST) ${reasons.postInjection}`);
+
+ let source = srcOrigin && srcOrigin !== "null" ? srcOrigin : "[...]";
+
+ let {button, option} = await Prompts.prompt({
+ title: _("XSS_promptTitle"),
+ message: _("XSS_promptMessage", [source, destOrigin, data.join(",")]),
+ options: [
+ {label: _(`XSS_opt${block ? 'Block' : 'Sanitize'}`), checked: true}, // 0
+ {label: _("XSS_optAlwaysBlock", [source, destOrigin])}, // 1
+ {label: _("XSS_optAllow")}, // 2
+ {label: _("XSS_optAlwaysAllow", [source, destOrigin])}, // 3
+ ],
+
+ buttons: [_("Ok")],
+ multiple: "focus",
+ width: 600,
+ height: 480,
+ });
+
+ if (button === 0 && option >= 2) {
+ if (option === 3) { // always allow
+ await XSS.setUserChoice(xssReq.originKey, "allow");
+ await XSS.saveUserChoices();
+ }
+ return ALLOW;
+ }
+ if (option === 1) { // always block
+ block = true;
+ await XSS.setUserChoice(xssReq.originKey, "block");
+ await XSS.saveUserChoices();
+ }
+ return block ? ABORT : ALLOW;
+ })();
+ promptsMap.set(xssReq.originKey, prompting);
+ try {
+ return await prompting;
+ } catch (e) {
+ error(e);
+ return ABORT;
+ }
+ };
+
+ return {
+ async start() {
+ let {onBeforeRequest} = browser.webRequest;
+ if (onBeforeRequest.hasListener(requestListener)) return;
+
+ await include("/legacy/Legacy.js");
+ await include("/xss/Exceptions.js");
+
+ this._userChoices = (await Storage.get("sync", "xssUserChoices")).xssUserChoices || {};
+
+ // conver old style whitelist if stored
+ let oldWhitelist = await XSS.Exceptions.getWhitelist();
+ if (oldWhitelist) {
+ for (let [destOrigin, sources] of Object.entries(oldWhitelist)) {
+ for (let srcOrigin of sources) {
+ this._userChoices[`${srcOrigin}>${destOrigin}`] = "allow";
+ }
+ }
+ XSS.Exceptions.setWhitelist(null);
+ }
+
+ onBeforeRequest.addListener(requestListener, {
+ urls: ["*://*/*"],
+ types: ["main_frame", "sub_frame", "object"]
+ }, ["blocking", "requestBody"]);
+ },
+
+ stop() {
+ let {onBeforeRequest} = browser.webRequest;
+ if (onBeforeRequest.hasListener(requestListener)) {
+ onBeforeRequest.removeListener(requestListener);
+ }
+ },
+
+
+ parseRequest(request) {
+ let {
+ url: destUrl,
+ originUrl: srcUrl,
+ method
+ } = request;
+ let destObj;
+ try {
+ destObj = new URL(destUrl);
+ } catch (e) {
+ error(e, "Cannot create URL object for %s", destUrl);
+ return null;
+ }
+ let srcObj = null;
+ if (srcUrl) {
+ try {
+ srcObj = new URL(srcUrl);
+ } catch (e) {}
+ } else {
+ srcUrl = "";
+ }
+
+ let unescapedDest = unescape(destUrl);
+ let srcOrigin = srcObj ? srcObj.origin : "";
+ let destOrigin = destObj.origin;
+
+ let isGet = method === "GET";
+ return {
+ xssUnparsed: request,
+ srcUrl,
+ destUrl,
+ srcObj,
+ destObj,
+ srcOrigin,
+ destOrigin,
+ get srcDomain() {
+ delete this.srcDomain;
+ return this.srcDomain = srcObj && srcObj.hostname && tld.getDomain(srcObj.hostname) || "";
+ },
+ get destDomain() {
+ delete this.destDomain;
+ return this.destDomain = tld.getDomain(destObj.hostname);
+ },
+ get originKey() {
+ delete this.originKey;
+ return this.originKey = `${srcOrigin}>${destOrigin}`;
+ },
+ unescapedDest,
+ isGet,
+ isPost: !isGet && method === "POST",
+ }
+ },
+
+ async saveUserChoices(xssUserChoices = this._userChoices || {}) {
+ this._userChoices = xssUserChoices;
+ await Storage.set("sync", {xssUserChoices});
+ },
+ getUserChoices() {
+ return this._userChoices;
+ },
+ setUserChoice(originKey, choice) {
+ this._userChoices[originKey] = choice;
+ },
+ getUserChoice(originKey) {
+ return this._userChoices[originKey];
+ },
+
+ async maybe(request) { // return reason or null if everything seems fine
+ let xssReq = request.xssUnparsed ? request : this.parseRequest(request);
+ request = xssReq.xssUnparsed;
+
+ if (await this.Exceptions.shouldIgnore(xssReq)) {
+ return null;
+ }
+
+ let {
+ skipParams,
+ skipRx
+ } = this.Exceptions.partial(xssReq);
+
+ let {destUrl} = xssReq;
+
+ await include("/xss/InjectionChecker.js");
+ let ic = await this.InjectionChecker;
+ ic.reset();
+
+ let postInjection = xssReq.isPost &&
+ request.requestBody && request.requestBody.formData &&
+ ic.checkPost(request.requestBody.formData, skipParams);
+
+ let protectName = ic.nameAssignment;
+ let urlInjection = ic.checkUrl(destUrl, skipRx);
+ protectName = protectName || ic.nameAssignment;
+ ic.reset();
+ return !(protectName || postInjection || urlInjection) ? null
+ : { protectName, postInjection, urlInjection };
+ }
+ };
+})();
diff --git a/src/xss/sanitizeName.js b/src/xss/sanitizeName.js
new file mode 100644
index 0000000..22185f4
--- /dev/null
+++ b/src/xss/sanitizeName.js
@@ -0,0 +1,4 @@
+if (/[<"'\`(=:]/.test(window.name)) {
+ console.log(`NoScript XSS filter sanitizing suspicious window.name "%s" on %s`, window.name, document.URL);
+ window.name = "";
+}