├── test ├── e2e │ └── test-index.js └── unit │ └── test-index.js ├── src ├── options │ ├── index.js │ └── index.html ├── images │ ├── app_icon_128.png │ ├── app_icon_16.png │ ├── icon-black.png │ ├── icon-green.png │ └── setting.svg ├── models │ ├── sink.js │ ├── source.js │ ├── trace.js │ ├── result.js │ └── extension-ui-actions.js ├── popup │ ├── detail.html │ ├── index.html │ ├── detail.js │ └── index.js ├── content-scripts │ └── index.js ├── manifest.json ├── styles │ └── style.css └── background │ ├── poc-checker.js │ ├── event-emitter.js │ ├── debugger.js │ ├── convert.js │ ├── interceptor.js │ ├── index.js │ └── preload.js ├── chrome-store ├── screen1.png ├── screen2.png ├── screen3.png ├── screen4.png └── screen5.png ├── .gitignore ├── .babelrc ├── LICENSE ├── README.md ├── scripts └── zip.js ├── package.json └── webpack.config.js /test/e2e/test-index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/unit/test-index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/index.js: -------------------------------------------------------------------------------- 1 | console.debug('options'); -------------------------------------------------------------------------------- /chrome-store/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsaiKen/dom-based-xss-finder/HEAD/chrome-store/screen1.png -------------------------------------------------------------------------------- /chrome-store/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsaiKen/dom-based-xss-finder/HEAD/chrome-store/screen2.png -------------------------------------------------------------------------------- /chrome-store/screen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsaiKen/dom-based-xss-finder/HEAD/chrome-store/screen3.png -------------------------------------------------------------------------------- /chrome-store/screen4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsaiKen/dom-based-xss-finder/HEAD/chrome-store/screen4.png -------------------------------------------------------------------------------- /chrome-store/screen5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsaiKen/dom-based-xss-finder/HEAD/chrome-store/screen5.png -------------------------------------------------------------------------------- /src/images/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsaiKen/dom-based-xss-finder/HEAD/src/images/app_icon_128.png -------------------------------------------------------------------------------- /src/images/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsaiKen/dom-based-xss-finder/HEAD/src/images/app_icon_16.png -------------------------------------------------------------------------------- /src/images/icon-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsaiKen/dom-based-xss-finder/HEAD/src/images/icon-black.png -------------------------------------------------------------------------------- /src/images/icon-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsaiKen/dom-based-xss-finder/HEAD/src/images/icon-green.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .idea 4 | build 5 | dist 6 | dist-zip 7 | *.pem 8 | *.crx 9 | tmp/ 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-transform-runtime" 7 | ] 8 | } -------------------------------------------------------------------------------- /src/models/sink.js: -------------------------------------------------------------------------------- 1 | export default class Sink { 2 | constructor({ label, stacktrace}) { 3 | this.label = label; 4 | this.stacktrace = stacktrace; 5 | } 6 | } -------------------------------------------------------------------------------- /src/models/source.js: -------------------------------------------------------------------------------- 1 | export default class Source { 2 | constructor({ label, stacktrace}) { 3 | this.label = label; 4 | this.stacktrace = stacktrace; 5 | } 6 | } -------------------------------------------------------------------------------- /src/models/trace.js: -------------------------------------------------------------------------------- 1 | export default class Trace { 2 | constructor() { 3 | this.url = null; 4 | this.line = null; 5 | this.column = null; 6 | this.code = null; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/models/result.js: -------------------------------------------------------------------------------- 1 | export default class Result { 2 | constructor({ id, url, source, sink }) { 3 | this.id = id; 4 | this.url = url; 5 | this.source = source; 6 | this.sink = sink; 7 | } 8 | } -------------------------------------------------------------------------------- /src/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Option for DOM based XSS finder 6 | 7 | 8 |

Option

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/models/extension-ui-actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | IS_RUNNING: 'IS_RUNNING', 3 | START: 'START', 4 | STOP: 'STOP', 5 | REMOVE: 'REMOVE', 6 | REMOVE_ALL: 'REMOVE_ALL', 7 | ADD_ALL: 'ADD_ALL', 8 | CHECK_AND_GENERATE_POC: 'CHECK_AND_GENERATE_POC', 9 | SET_POC: 'SET_POC', 10 | }; 11 | -------------------------------------------------------------------------------- /src/popup/detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 |
12 |

Source CallStack

13 | 14 | 15 |

Sink CallStack

16 | 17 | 18 |
19 |

PoC URL

20 |
21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/content-scripts/index.js: -------------------------------------------------------------------------------- 1 | import actions from '../models/extension-ui-actions'; 2 | 3 | if (!window.__dombasedxssfinder_contentscript) { 4 | window.__dombasedxssfinder_contentscript = true; 5 | setInterval(() => { 6 | const elements = document.querySelectorAll('.__dombasedxssfinder_result'); 7 | if (elements.length > 0) { 8 | console.debug(elements); 9 | } 10 | const results = []; 11 | for (const e of elements) { 12 | const result = JSON.parse(e.textContent); 13 | if (/jquery|google/.test(result.source.stacktrace[0].url)) { 14 | continue; 15 | } 16 | results.push(result); 17 | } 18 | for (const e of elements) { 19 | e.parentElement.removeChild(e); 20 | } 21 | if (results.length > 0) { 22 | chrome.runtime.sendMessage({ action: actions.ADD_ALL, results }); 23 | } 24 | }, 100); 25 | console.debug(`content-scripts at ${location.href}`); 26 | } -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DOM based XSS finder", 3 | "version": "1.0.0", 4 | "manifest_version": 2, 5 | "description": "A Chrome extension for finding DOM based XSS vulnerabilities", 6 | "permissions": [ 7 | "storage", 8 | "webNavigation", 9 | "tabs", 10 | "*://*/", 11 | "debugger", 12 | "unlimitedStorage" 13 | ], 14 | "icons" : { 15 | "16": "images/app_icon_16.png", 16 | "128": "images/app_icon_128.png" 17 | }, 18 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", 19 | "browser_action": { 20 | "default_icon": "images/icon-black.png", 21 | "default_title": "DOM based XSS finder", 22 | "default_popup": "popup.html" 23 | }, 24 | "background": { 25 | "scripts": [ 26 | "background.js" 27 | ], 28 | "persistent": true 29 | }, 30 | "options_ui": { 31 | "page": "options.html", 32 | "open_in_tab": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Ken Asai 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DOM based XSS finder 2 | 3 | "DOM based XSS finder" is a Chrome extension that finds DOM based XSS vulnerabilities. Install it from the [Chrome Webstore](https://chrome.google.com/webstore/detail/dom-based-xss-finder/ngmdldjheklkdchgkgnjoaabgejcnnoi). 4 | 5 | Finding DOM based XSS can be bothersome. This extension can be helpful. This extension has the following features: 6 | 7 | - Notify if a user-input such as "location.href" leads to a dangerous JavaScript function such as "eval". 8 | - Fuzzing for user-inputs such as query, hash and referrer. 9 | - Generate a PoC that generates a alert prompt. 10 | 11 | ## Usage 12 | 13 | **This tool is a dynamic JavaScript tracer, not a static JavaScript scanner. So you must execute JavaScript by manual 14 | crawling with this extension starting.** 15 | 16 | - Click the icon and hit "Start". 17 | - Browse pages that you want to scan. 18 | - If the extension finds a possible vulnerability of DOM based XSS, the extension shows a entry for that url. 19 | - Click "Detail" in the entry. A popup window show a source and a sink of the possible vulnerability. 20 | - Click "Check and Generate PoC" in the popup window. You can fuzzing the url. 21 | 22 | ## License 23 | 24 | MIT 25 | -------------------------------------------------------------------------------- /scripts/zip.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const zipFolder = require('zip-folder'); 5 | 6 | const DEST_DIR = path.join(__dirname, '../dist'); 7 | const DEST_ZIP_DIR = path.join(__dirname, '../dist-zip'); 8 | 9 | const extractExtensionData = () => { 10 | const extPackageJson = require('../package.json'); 11 | 12 | return { 13 | name: extPackageJson.name, 14 | version: extPackageJson.version 15 | }; 16 | }; 17 | 18 | const makeDestZipDirIfNotExists = () => { 19 | if (!fs.existsSync(DEST_ZIP_DIR)) { 20 | fs.mkdirSync(DEST_ZIP_DIR); 21 | } 22 | }; 23 | 24 | const buildZip = (src, dist, zipFilename) => { 25 | console.info(`Building ${zipFilename}...`); 26 | 27 | return new Promise((resolve, reject) => { 28 | zipFolder(src, path.join(dist, zipFilename), (err) => { 29 | if (err) { 30 | reject(err); 31 | } else { 32 | resolve(); 33 | } 34 | }); 35 | }); 36 | }; 37 | 38 | const main = () => { 39 | const { name, version } = extractExtensionData(); 40 | const zipFilename = `${name}-v${version}.zip`; 41 | 42 | makeDestZipDirIfNotExists(); 43 | 44 | buildZip(DEST_DIR, DEST_ZIP_DIR, zipFilename) 45 | .then(() => console.info('OK')) 46 | .catch(console.err); 47 | }; 48 | 49 | main(); 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dom-based-xss-finder", 3 | "version": "1.0.0", 4 | "description": "A Chrome extension for finding DOM based XSS vulnerabilities", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "NODE_ENV=development DEBUG=dom-based-xss-finder:* webpack --watch", 8 | "build": "NODE_ENV=production webpack", 9 | "dist": "NODE_ENV=production webpack && node scripts/zip.js", 10 | "test": "mocha test/**/test-*.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/AsaiKen/dom-based-xss-finder" 15 | }, 16 | "keywords": [ 17 | "chrome", 18 | "extension", 19 | "xss" 20 | ], 21 | "author": "Ken Asai", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@babel/runtime": "^7.7.7" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.7.7", 28 | "@babel/plugin-transform-runtime": "^7.7.6", 29 | "@babel/preset-env": "^7.7.7", 30 | "@fortawesome/fontawesome-free": "^5.12.0", 31 | "babel-loader": "^8.0.6", 32 | "bootstrap": "^4.4.1", 33 | "chai": "^4.2.0", 34 | "copy-webpack-plugin": "^5.1.1", 35 | "css-loader": "^3.4.0", 36 | "encoding-japanese": "^1.0.30", 37 | "html-webpack-plugin": "^3.2.0", 38 | "iconv-lite": "^0.5.0", 39 | "jquery": "^3.4.1", 40 | "mocha": "^6.2.2", 41 | "popper.js": "^1.16.0", 42 | "source-map": "^0.7.3", 43 | "style-loader": "^1.1.2", 44 | "webpack": "^4.41.5", 45 | "webpack-cli": "^3.3.10", 46 | "zip-folder": "^1.0.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-size: 100%; 4 | color: #3C4858; 5 | width: 350px; 6 | max-height: 500px; 7 | } 8 | 9 | .header { 10 | background: #F9FAFC; 11 | height: 90px; 12 | display: flex; 13 | justify-content: flex-start; 14 | align-items: center; 15 | padding: 0 12px; 16 | font-weight: 500; 17 | } 18 | 19 | .main { 20 | font-size: 14px; 21 | padding: 0 12px; 22 | } 23 | 24 | .result { 25 | max-height: 400px; 26 | flex: 1; 27 | height: 100%; 28 | overflow: auto; 29 | overflow-x: hidden; 30 | display: flex; 31 | flex-direction: column-reverse; 32 | } 33 | 34 | .callstack { 35 | font-size: 15px; 36 | } 37 | 38 | button.btn { 39 | display: inline-block; 40 | font-weight: 400; 41 | line-height: 1.25; 42 | text-align: center; 43 | white-space: nowrap; 44 | vertical-align: middle; 45 | user-select: none; 46 | border: 1px solid transparent; 47 | cursor: pointer; 48 | letter-spacing: 1px; 49 | transition: all .15s ease; 50 | } 51 | 52 | button.btn.btn-sm { 53 | padding: .4rem .8rem; 54 | font-size: .8rem; 55 | border-radius: .2rem; 56 | height: 30px; 57 | } 58 | 59 | button.btn.btn-primary, button.btn.btn-primary:active:focus { 60 | color: #fff; 61 | background-color: #45C8F1; 62 | border-color: #45C8F1; 63 | } 64 | 65 | button.btn.btn-outline-primary, button.btn.btn-outline-primary:active:focus { 66 | color: #45C8F1; 67 | background-color: transparent; 68 | border-color: #45C8F1; 69 | } 70 | 71 | button.btn.btn-danger, button.btn.btn-danger:active:focus { 72 | color: #fff; 73 | background-color: #FF4949; 74 | border-color: #FF4949; 75 | } 76 | 77 | button.btn.btn-outline-danger, button.btn.btn-outline-danger:active:focus { 78 | color: #FF4949; 79 | background-color: transparent; 80 | border-color: #FF4949; 81 | } 82 | -------------------------------------------------------------------------------- /src/background/poc-checker.js: -------------------------------------------------------------------------------- 1 | import Debugger from './debugger'; 2 | 3 | export default class PocChecker { 4 | constructor(windowId, tabId, url, keyword) { 5 | this.windowId = windowId; 6 | this.tabId = tabId; 7 | this.debugger_ = new Debugger(tabId); 8 | this.url = url; 9 | this.keyword = keyword; 10 | this.detect = false; 11 | this._isIdle = false; 12 | } 13 | 14 | async start() { 15 | const debugger_ = this.debugger_; 16 | try { 17 | await debugger_.attach(); 18 | console.debug('attached', { tabId: this.tabId }); 19 | } catch (e) { 20 | console.error(e); 21 | return; 22 | } 23 | 24 | await debugger_.sendCommand('Page.enable'); 25 | await debugger_.on('Page.javascriptDialogOpening', async({ message }) => { 26 | console.debug('message', message); 27 | if (message.includes(this.keyword)) { 28 | this.detect = true; 29 | } 30 | await debugger_.sendCommand('Page.handleJavaScriptDialog', { accept: true }); 31 | }); 32 | await debugger_.sendCommand('Page.setLifecycleEventsEnabled', { enabled: true }); 33 | await debugger_.on('Page.lifecycleEvent', async({ name }) => { 34 | console.debug('lifecycleEvent', name); 35 | if (name === 'networkAlmostIdle') { 36 | await this.setIdle(); 37 | } 38 | }); 39 | await debugger_.sendCommand('Page.navigate', { url: this.url }); 40 | console.debug('start', this.windowId); 41 | } 42 | 43 | isIdle() { 44 | return this._isIdle; 45 | } 46 | 47 | async setIdle() { 48 | console.debug('setIdle'); 49 | this._isIdle = true; 50 | } 51 | 52 | stop() { 53 | return new Promise(async resolve => { 54 | chrome.windows.remove(this.windowId, () => { 55 | console.debug('remove', this.windowId); 56 | resolve(); 57 | }); 58 | }); 59 | } 60 | }; -------------------------------------------------------------------------------- /src/background/event-emitter.js: -------------------------------------------------------------------------------- 1 | //https://gist.github.com/mudge/5830382 2 | 3 | /* Polyfill indexOf. */ 4 | var indexOf; 5 | 6 | if (typeof Array.prototype.indexOf === 'function') { 7 | indexOf = function(haystack, needle) { 8 | return haystack.indexOf(needle); 9 | }; 10 | } else { 11 | indexOf = function(haystack, needle) { 12 | var i = 0, length = haystack.length, idx = -1, found = false; 13 | 14 | while (i < length && !found) { 15 | if (haystack[i] === needle) { 16 | idx = i; 17 | found = true; 18 | } 19 | 20 | i++; 21 | } 22 | 23 | return idx; 24 | }; 25 | } 26 | 27 | 28 | /* Polyfill EventEmitter. */ 29 | var EventEmitter = function() { 30 | this.events = {}; 31 | }; 32 | 33 | EventEmitter.prototype.on = function(event, listener) { 34 | if (typeof this.events[event] !== 'object') { 35 | this.events[event] = []; 36 | } 37 | 38 | this.events[event].push(listener); 39 | }; 40 | 41 | EventEmitter.prototype.removeListener = function(event, listener) { 42 | var idx; 43 | 44 | if (typeof this.events[event] === 'object') { 45 | idx = indexOf(this.events[event], listener); 46 | 47 | if (idx > -1) { 48 | this.events[event].splice(idx, 1); 49 | } 50 | } 51 | }; 52 | 53 | EventEmitter.prototype.emit = function(event) { 54 | var i, listeners, length, args = [].slice.call(arguments, 1); 55 | 56 | if (typeof this.events[event] === 'object') { 57 | listeners = this.events[event].slice(); 58 | length = listeners.length; 59 | 60 | for (i = 0; i < length; i++) { 61 | listeners[i].apply(this, args); 62 | } 63 | } 64 | }; 65 | 66 | EventEmitter.prototype.once = function(event, listener) { 67 | this.on(event, function g() { 68 | this.removeListener(event, g); 69 | listener.apply(this, arguments); 70 | }); 71 | }; 72 | 73 | export default EventEmitter; -------------------------------------------------------------------------------- /src/background/debugger.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from './event-emitter'; 2 | 3 | const DEBUGGER_PROTOCOL_VERSION = '1.3'; 4 | 5 | export default class Debugger extends EventEmitter { 6 | constructor(tabId) { 7 | super(); 8 | this.tabId = tabId; 9 | this.boundOnEventHandler = this.onEventHandler.bind(this); 10 | } 11 | 12 | attach() { 13 | return new Promise((resolve, reject) => { 14 | const target = { tabId: this.tabId }; 15 | chrome.debugger.attach(target, DEBUGGER_PROTOCOL_VERSION, () => { 16 | if (chrome.runtime.lastError) { 17 | //attached 18 | reject(chrome.runtime.lastError); 19 | return; 20 | } 21 | chrome.debugger.onEvent.addListener(this.boundOnEventHandler); 22 | resolve(); 23 | }); 24 | }); 25 | } 26 | 27 | onEventHandler(source, method, params) { 28 | // console.debug(source, method, params); 29 | if (source.tabId === this.tabId) { 30 | this.emit(method, params); 31 | } 32 | } 33 | 34 | detach() { 35 | return new Promise((resolve, reject) => { 36 | chrome.debugger.onEvent.removeListener(this.boundOnEventHandler); 37 | const target = { tabId: this.tabId }; 38 | chrome.debugger.detach(target, () => { 39 | if (chrome.runtime.lastError && chrome.runtime.lastError.message) { 40 | console.debug(chrome.runtime.lastError.message); 41 | } 42 | resolve(); 43 | }); 44 | }); 45 | } 46 | 47 | sendCommand(command, params) { 48 | return new Promise((resolve, reject) => { 49 | const target = { tabId: this.tabId }; 50 | chrome.debugger.sendCommand(target, command, params, result => { 51 | if (chrome.runtime.lastError && chrome.runtime.lastError.message) { 52 | console.debug(chrome.runtime.lastError.message); 53 | } 54 | resolve(result); 55 | }); 56 | }); 57 | } 58 | }; -------------------------------------------------------------------------------- /src/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 |
10 |
DOM based XSS finder
11 |
12 | 13 |
14 |
15 |
16 |
17 |
18 | 21 |
22 |
23 | 28 |
29 |
30 |
31 | 32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | 57 |
58 |
59 |
60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/images/setting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | _ionicons_svg_ios-settings 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | const { NODE_ENV = 'development' } = process.env; 7 | 8 | const base = { 9 | context: __dirname, 10 | entry: { 11 | background: './src/background/index.js', 12 | 'content-script': './src/content-scripts/index.js', 13 | popup: './src/popup/index.js', 14 | detail: './src/popup/detail.js', 15 | options: './src/options/index.js' 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.js$/, 21 | exclude: /node_modules/, 22 | loader: 'babel-loader' 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: [ 27 | 'style-loader', 28 | 'css-loader' 29 | ] 30 | } 31 | ] 32 | }, 33 | plugins: [ 34 | new CopyPlugin([ 35 | { from: './src/manifest.json', to: './manifest.json' }, 36 | { from: './src/images', to: 'images' }, 37 | { from: './src/background/preload.js', to: './preload.js' } 38 | ]), 39 | new HtmlWebpackPlugin({ 40 | template: './src/popup/index.html', 41 | chunks: ['popup'], 42 | filename: 'popup.html' 43 | }), 44 | new HtmlWebpackPlugin({ 45 | template: './src/options/index.html', 46 | chunks: ['options'], 47 | filename: 'options.html' 48 | }), 49 | new HtmlWebpackPlugin({ 50 | template: './src/popup/detail.html', 51 | chunks: ['detail'], 52 | filename: 'detail.html' 53 | }), 54 | new webpack.DefinePlugin({ 55 | 'process.env': { 56 | NODE_ENV: JSON.stringify(NODE_ENV) 57 | } 58 | }) 59 | ], 60 | node: { 61 | fs: "empty" 62 | } 63 | }; 64 | 65 | const development = { 66 | ...base, 67 | output: { 68 | path: path.join(__dirname, 'build'), 69 | filename: '[name].js' 70 | }, 71 | mode: 'development', 72 | devtool: '#eval-module-source-map', 73 | plugins: [ 74 | ...base.plugins, 75 | new webpack.HotModuleReplacementPlugin() 76 | ] 77 | }; 78 | 79 | const production = { 80 | ...base, 81 | output: { 82 | path: path.join(__dirname, 'dist'), 83 | filename: '[name].js' 84 | }, 85 | mode: 'production', 86 | devtool: '#source-map', 87 | plugins: [ 88 | ...base.plugins, 89 | new webpack.LoaderOptionsPlugin({ 90 | minimize: true, 91 | debug: false 92 | }) 93 | ] 94 | }; 95 | 96 | if (NODE_ENV === 'development') { 97 | module.exports = development; 98 | } else { 99 | module.exports = production; 100 | } 101 | -------------------------------------------------------------------------------- /src/popup/detail.js: -------------------------------------------------------------------------------- 1 | import "bootstrap/dist/css/bootstrap.min.css"; 2 | import "bootstrap"; 3 | import "../styles/style.css"; 4 | import actions from '../models/extension-ui-actions'; 5 | 6 | const resultId = new URL(location.href).searchParams.get('resultId'); 7 | 8 | const pocButton = document.querySelector('#poc-button'); 9 | pocButton.addEventListener('click', function() { 10 | chrome.runtime.sendMessage({ action: actions.CHECK_AND_GENERATE_POC, resultId }); 11 | }); 12 | 13 | function setPocUrl(url) { 14 | const pocDiv = document.getElementById('poc'); 15 | pocDiv.classList.remove('d-none'); 16 | const pocUrlDiv = document.getElementById('poc-url'); 17 | if (url) { 18 | [...pocUrlDiv.children].forEach(c => pocUrlDiv.removeChild(c)); 19 | const anchor = document.createElement('a'); 20 | anchor.href = url; 21 | anchor.target = '_blank'; 22 | anchor.textContent = url; 23 | pocUrlDiv.appendChild(anchor); 24 | } else { 25 | pocUrlDiv.textContent = 'Not Found'; 26 | } 27 | } 28 | 29 | chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { 30 | if (msg.action === actions.SET_POC && msg.resultId === Number(resultId)) { 31 | const pocUrl = msg.pocUrl; 32 | setPocUrl(pocUrl); 33 | sendResponse(); 34 | } 35 | }); 36 | 37 | const removeButton = document.querySelector('#remove-button'); 38 | removeButton.addEventListener('click', function() { 39 | chrome.runtime.sendMessage({ action: actions.REMOVE, resultId }, () => { 40 | window.close(); 41 | }); 42 | }); 43 | 44 | chrome.storage.local.get('results', async items => { 45 | let results = items.results || []; 46 | const result = results.find(r => r.id === Number(resultId)); 47 | if (result) { 48 | document.title = `#${result.id} ${result.url}`; 49 | 50 | const sourceDiv = document.getElementById('source'); 51 | const source = result.source; 52 | document.getElementById('source-label').textContent = source.label; 53 | const sourceStacktrace = source.stacktrace; 54 | for (let i = 0; i < sourceStacktrace.length; i++) { 55 | const trace = sourceStacktrace[i]; 56 | const posDiv = document.createElement('li'); 57 | posDiv.textContent = `at ${trace.url}:${trace.line}:${trace.column}`; 58 | sourceDiv.appendChild(posDiv); 59 | const codeDiv = document.createElement('code'); 60 | codeDiv.textContent = `${trace.code}`; 61 | sourceDiv.appendChild(codeDiv); 62 | } 63 | 64 | const sinkDiv = document.getElementById('sink'); 65 | const sink = result.sink; 66 | document.getElementById('sink-label').textContent = sink.label; 67 | const sinkStacktrace = sink.stacktrace; 68 | for (let i = 0; i < sinkStacktrace.length; i++) { 69 | const trace = sinkStacktrace[i]; 70 | const posDiv = document.createElement('li'); 71 | posDiv.textContent = `at ${trace.url}:${trace.line}:${trace.column}`; 72 | sinkDiv.appendChild(posDiv); 73 | const codeDiv = document.createElement('code'); 74 | codeDiv.textContent = `${trace.code}`; 75 | sinkDiv.appendChild(codeDiv); 76 | } 77 | 78 | if (Object.keys(result).includes('pocUrl')) { 79 | setPocUrl(result.pocUrl); 80 | } 81 | } 82 | }); 83 | -------------------------------------------------------------------------------- /src/popup/index.js: -------------------------------------------------------------------------------- 1 | import "bootstrap/dist/css/bootstrap.min.css"; 2 | import "bootstrap"; 3 | import "../styles/style.css"; 4 | import '@fortawesome/fontawesome-free/js/fontawesome'; 5 | import '@fortawesome/fontawesome-free/js/solid'; 6 | import '@fortawesome/fontawesome-free/js/regular'; 7 | import actions from '../models/extension-ui-actions'; 8 | import {name} from '../../package'; 9 | 10 | class Popup { 11 | constructor() { 12 | this.port = chrome.extension.connect({ name }); 13 | this.startButton = document.querySelector('#enable-button'); 14 | this.stopButton = document.querySelector('#disable-button'); 15 | // this.settingButton = document.querySelector('#setting-button'); 16 | this.removeAllButton = document.querySelector('#remove-all-button'); 17 | 18 | this.port.postMessage({ action: actions.IS_RUNNING }); 19 | this.port.onMessage.addListener(({ isRunning }) => { 20 | if (typeof isRunning === 'boolean') { 21 | if (isRunning) { 22 | this.showStartButton(); 23 | } else { 24 | this.showStopButton(); 25 | } 26 | } 27 | }); 28 | 29 | this.startButton.addEventListener('click', () => { 30 | this.start(); 31 | }); 32 | this.stopButton.addEventListener('click', () => { 33 | this.stop(); 34 | }); 35 | 36 | // this.settingButton.addEventListener('click', () => { 37 | // chrome.runtime.openOptionsPage(); 38 | // }); 39 | 40 | this.removeAllButton.addEventListener('click', () => { 41 | this.removeAll(); 42 | }); 43 | 44 | this.loadResults(); 45 | } 46 | 47 | loadResults() { 48 | chrome.storage.local.get('results', items => { 49 | const results = items.results || []; 50 | for (const result of results.sort((a, b) => a.id - b.id)) { 51 | this.add(result); 52 | } 53 | }); 54 | } 55 | 56 | showStartButton() { 57 | this.startButton.classList.add('d-none'); 58 | this.stopButton.classList.remove('d-none'); 59 | } 60 | 61 | start() { 62 | this.showStartButton(); 63 | this.port.postMessage({ action: actions.START }); 64 | } 65 | 66 | showStopButton() { 67 | this.startButton.classList.remove('d-none'); 68 | this.stopButton.classList.add('d-none'); 69 | } 70 | 71 | stop() { 72 | this.showStopButton(); 73 | this.port.postMessage({ action: actions.STOP }); 74 | } 75 | 76 | removeAll() { 77 | console.debug('removeAll'); 78 | this.port.postMessage({ action: actions.REMOVE_ALL }); 79 | const results = document.querySelectorAll('.row-result'); 80 | for (const result of results) { 81 | result.parentElement.removeChild(result); 82 | } 83 | } 84 | 85 | add(result) { 86 | const id = result.id; 87 | const url = result.url; 88 | const resultContainer = document.querySelector('#container-result'); 89 | const template = document.querySelector('#row-result-template'); 90 | const content = template.content; 91 | content.querySelector('.row-result').id = 'result-' + id; 92 | content.querySelector('.col-result-id').innerText = id; 93 | const resultUrl = content.querySelector('.col-result-url'); 94 | resultUrl.textContent = url; 95 | content.querySelector('.result-detail-button').dataset.resultId = id; 96 | content.querySelector('.result-remove-button').dataset.resultId = id; 97 | const element = document.importNode(content, true); 98 | const detailButton = element.querySelector('.result-detail-button'); 99 | detailButton.addEventListener('click', () => { 100 | this.detail(result.id); 101 | }); 102 | const removeButton = element.querySelector('.result-remove-button'); 103 | removeButton.addEventListener('click', () => { 104 | this.remove(result.id); 105 | }); 106 | resultContainer.appendChild(element); 107 | } 108 | 109 | detail(resultId) { 110 | chrome.windows.create({ 111 | url: chrome.runtime.getURL("detail.html") + '?resultId=' + resultId, 112 | type: "popup" 113 | }); 114 | } 115 | 116 | remove(resultId) { 117 | this.port.postMessage({ action: actions.REMOVE, resultId }); 118 | const result = document.getElementById('result-' + resultId); 119 | if (result) { 120 | result.parentElement.removeChild(result); 121 | } 122 | } 123 | } 124 | 125 | window.__dombasedxssfinder_popup = new Popup(); 126 | -------------------------------------------------------------------------------- /src/background/convert.js: -------------------------------------------------------------------------------- 1 | import {transform} from '@babel/core'; 2 | 3 | export default function(src) { 4 | const visitedKey = '__dombasedxssfinder_visited_key__'; 5 | const plugin = ({ types: t }) => { 6 | function callExpression(callee, arguments_) { 7 | const e = t.callExpression(callee, arguments_); 8 | e[visitedKey] = true; 9 | return e; 10 | } 11 | 12 | const visitor = { 13 | BinaryExpression: { 14 | enter: (nodePath) => { 15 | if (nodePath[visitedKey] || nodePath.node[visitedKey]) { 16 | return; 17 | } 18 | 19 | const { left, operator, right } = nodePath.node; 20 | let newAst; 21 | if (operator === '+') { 22 | // a + b -> __dombasedxssfinder_plus(a, b) 23 | newAst = callExpression( 24 | t.identifier('__dombasedxssfinder_plus'), 25 | [left, right] 26 | ); 27 | } else if (operator === '==') { 28 | newAst = callExpression( 29 | t.identifier('__dombasedxssfinder_equal'), 30 | [left, right] 31 | ); 32 | } else if (operator === '!=') { 33 | newAst = callExpression( 34 | t.identifier('__dombasedxssfinder_notEqual'), 35 | [left, right] 36 | ); 37 | } else if (operator === '===') { 38 | newAst = callExpression( 39 | t.identifier('__dombasedxssfinder_strictEqual'), 40 | [left, right] 41 | ); 42 | } else if (operator === '!==') { 43 | newAst = callExpression( 44 | t.identifier('__dombasedxssfinder_strictNotEqual'), 45 | [left, right] 46 | ); 47 | } 48 | if (newAst) { 49 | nodePath.replaceWith(newAst); 50 | nodePath[visitedKey] = true; 51 | } 52 | }, 53 | }, 54 | AssignmentExpression: { 55 | enter: (nodePath) => { 56 | if (nodePath[visitedKey] || nodePath.node[visitedKey]) { 57 | return; 58 | } 59 | 60 | let { left, operator, right } = nodePath.node; 61 | if (operator === '+=') { 62 | // a += b -> a = __dombasedxssfinder_plus(a, b) 63 | right = callExpression( 64 | t.identifier('__dombasedxssfinder_plus'), 65 | [left, right] 66 | ); 67 | } else if (operator.length >= 2 && operator.endsWith('=')) { 68 | const subOp = operator.slice(0, -1); 69 | // a -= b -> a = a - b 70 | right = t.binaryExpression(subOp, left, right); 71 | } 72 | let newAst; 73 | if (left.type === 'MemberExpression') { 74 | // a.b = c -> __dombasedxssfinder_put(a, b, c) 75 | const { object, property, computed } = left; 76 | let key; 77 | if (computed) { // a[b], a['b'] 78 | key = property; 79 | } else { // a.b 80 | key = t.stringLiteral(property.name); 81 | } 82 | newAst = callExpression( 83 | t.identifier('__dombasedxssfinder_put'), 84 | [object, key, right] 85 | ); 86 | } else { 87 | const assignmentExpression = t.assignmentExpression("=", left, right); 88 | assignmentExpression[visitedKey] = true; 89 | newAst = assignmentExpression; 90 | } 91 | nodePath.replaceWith(newAst); 92 | nodePath[visitedKey] = true; 93 | } 94 | }, 95 | MemberExpression: { 96 | enter: (nodePath) => { 97 | if (nodePath[visitedKey] || nodePath.node[visitedKey]) { 98 | return; 99 | } 100 | 101 | const { object, property, computed } = nodePath.node; 102 | let key; 103 | if (computed) { // a[b], a['b'] 104 | key = property; 105 | } else { // a.b 106 | key = t.stringLiteral(property.name); 107 | } 108 | const newAst = callExpression( 109 | t.identifier('__dombasedxssfinder_get'), 110 | [object, key] 111 | ); 112 | nodePath.replaceWith(newAst); 113 | nodePath[visitedKey] = true; 114 | } 115 | }, 116 | NewExpression: { 117 | enter: (nodePath) => { 118 | if (nodePath[visitedKey] || nodePath.node[visitedKey]) { 119 | return; 120 | } 121 | 122 | const o = nodePath.node; 123 | const callee = o.callee; 124 | const arguments_ = o.arguments; 125 | if (callee.name === 'Function') { 126 | const newAst = callExpression( 127 | t.identifier('__dombasedxssfinder_new_Function'), 128 | arguments_ 129 | ); 130 | nodePath.replaceWith(newAst); 131 | nodePath[visitedKey] = true; 132 | } 133 | } 134 | }, 135 | UnaryExpression: { 136 | enter: (nodePath) => { 137 | if (nodePath[visitedKey] || nodePath.node[visitedKey]) { 138 | return; 139 | } 140 | 141 | const { operator, argument } = nodePath.node; 142 | if (operator === 'typeof') { 143 | let newAst; 144 | if (argument.type === 'Identifier') { 145 | const unaryExpression = t.unaryExpression('typeof', argument, true); 146 | unaryExpression[visitedKey] = true; 147 | const binaryExpression = t.binaryExpression('===', unaryExpression, t.stringLiteral('undefined')); 148 | binaryExpression[visitedKey] = true; 149 | newAst = callExpression( 150 | t.identifier('__dombasedxssfinder_typeof'), 151 | [ 152 | // aが未定義の場合、typeof aは通過するが、f(a)はエラーになる。その対応。 153 | t.conditionalExpression( 154 | binaryExpression, 155 | t.identifier('undefined'), 156 | argument 157 | ) 158 | ] 159 | ); 160 | } else { 161 | newAst = callExpression( 162 | t.identifier('__dombasedxssfinder_typeof'), 163 | [argument] 164 | ); 165 | } 166 | nodePath.replaceWith(newAst); 167 | nodePath[visitedKey] = true; 168 | } else if (operator === 'delete') { 169 | if (argument.type === 'MemberExpression') { 170 | // delete __dombasedxssfinder_get(a, 'b')だとdeleteされないので、MemberExpressionを残す 171 | argument[visitedKey] = true; 172 | } 173 | } 174 | } 175 | }, 176 | CallExpression: { 177 | enter: (nodePath) => { 178 | if (nodePath[visitedKey] || nodePath.node[visitedKey]) { 179 | return; 180 | } 181 | 182 | const o = nodePath.node; 183 | const callee = o.callee; 184 | const arguments_ = o.arguments; 185 | let newAst; 186 | if (callee.type === 'MemberExpression') { 187 | const { object, property, computed } = callee; 188 | let key; 189 | if (computed) { // a[b], a['b'] 190 | key = property; 191 | } else { // a.b 192 | key = t.stringLiteral(property.name); 193 | } 194 | newAst = callExpression( 195 | t.identifier('__dombasedxssfinder_property_call'), 196 | [object, key, ...arguments_] 197 | ); 198 | } else { 199 | newAst = callExpression( 200 | t.identifier('__dombasedxssfinder_call'), 201 | [callee, ...arguments_] 202 | ); 203 | } 204 | nodePath.replaceWith(newAst); 205 | nodePath[visitedKey] = true; 206 | } 207 | }, 208 | UpdateExpression: { 209 | enter: (nodePath) => { 210 | if (nodePath[visitedKey] || nodePath.node[visitedKey]) { 211 | return; 212 | } 213 | 214 | const { argument } = nodePath.node; 215 | if (argument.type === 'MemberExpression') { 216 | // __dombasedxssfinder_get(this, "activeNums")++;はエラーになるので、MemberExpressionを残す 217 | argument[visitedKey] = true; 218 | } 219 | } 220 | }, 221 | }; 222 | return { visitor }; 223 | }; 224 | 225 | try { 226 | const { code, map } = transform(src, { 227 | parserOpts: { strictMode: false }, 228 | plugins: [plugin], 229 | configFile: false, 230 | sourceMaps: true, 231 | retainLines: true, 232 | compact: false, 233 | }); 234 | // console.debug('map', map); 235 | return { code, map }; 236 | } catch (e) { 237 | console.error(e); 238 | return src; 239 | } 240 | }; -------------------------------------------------------------------------------- /src/background/interceptor.js: -------------------------------------------------------------------------------- 1 | import Debugger from './debugger'; 2 | import convert from './convert'; 3 | import iconv from 'iconv-lite'; 4 | import Encoding from 'encoding-japanese'; 5 | 6 | let PRELOAD_SOURCE = null; 7 | 8 | export default class Interceptor { 9 | constructor() { 10 | this.isRunning = false; 11 | /** @type {Debugger[]} */ 12 | this.debuggers = []; 13 | this.sourceMaps = {}; 14 | this.jsCache = {}; 15 | this.bodyMaps = {}; 16 | 17 | this.boundOnCreatedHandler = this.onCreatedHandler.bind(this); 18 | this.boundOnBeforeNavigateHandler = this.onBeforeNavigateHandler.bind(this); 19 | this.boundOnDOMContentLoadedHandler = this.onDOMContentLoadedHandler.bind(this); 20 | this.boundOnDetachHandler = this.onDetachHandler.bind(this); 21 | } 22 | 23 | async start() { 24 | if (PRELOAD_SOURCE === null) { 25 | const url = chrome.runtime.getURL("preload.js"); 26 | const response = await fetch(url); 27 | PRELOAD_SOURCE = await response.text(); 28 | } 29 | this.isRunning = true; 30 | chrome.windows.getAll({ populate: true }, async windows => { 31 | for (const w of windows) { 32 | for (const tab of w.tabs) { 33 | // console.debug('tab', tab); 34 | await this.attach({ tabId: tab.id, url: tab.url }); 35 | } 36 | } 37 | }); 38 | chrome.tabs.onCreated.addListener(this.boundOnCreatedHandler); 39 | chrome.webNavigation.onBeforeNavigate.addListener(this.boundOnBeforeNavigateHandler); 40 | chrome.webNavigation.onDOMContentLoaded.addListener(this.boundOnDOMContentLoadedHandler); 41 | chrome.debugger.onDetach.addListener(this.boundOnDetachHandler); 42 | } 43 | 44 | async onCreatedHandler({ id, url }) { 45 | console.debug('onCreated', { id, url }); 46 | await this.attach({ tabId: id, url }); 47 | } 48 | 49 | async onBeforeNavigateHandler({ tabId, frameId, url }) { 50 | console.debug('onBeforeNavigate', { tabId, frameId, url }); 51 | if (frameId === 0) { 52 | // mainframe navigated 53 | for (const key of Object.keys(this.sourceMaps)) { 54 | const o = JSON.parse(key); 55 | if (o.tabId === tabId) { 56 | // console.debug('remove sourceMaps', key); 57 | delete this.sourceMaps[key]; 58 | } 59 | } 60 | for (const key of Object.keys(this.bodyMaps)) { 61 | const o = JSON.parse(key); 62 | if (o.tabId === tabId) { 63 | // console.debug('remove sourceMaps', key); 64 | delete this.bodyMaps[key]; 65 | } 66 | } 67 | } 68 | } 69 | 70 | async onDOMContentLoadedHandler({ tabId, url }) { 71 | console.debug('onDOMContentLoaded', { tabId, url }); 72 | chrome.tabs.executeScript(tabId, { file: 'content-script.js', allFrames: true }); 73 | } 74 | 75 | async onDetachHandler({ tabId }) { 76 | console.debug('onDetach', { tabId }); 77 | if (this.isRunning) { 78 | for (const debugger_ of this.debuggers) { 79 | if (debugger_.tabId === tabId) { 80 | chrome.tabs.get(tabId, async() => { 81 | if (chrome.runtime.lastError) { 82 | // closed 83 | return; 84 | } 85 | try { 86 | await debugger_.attach(); 87 | console.debug('re-attached', { tabId }); 88 | } catch (e) { 89 | console.error(e); 90 | } 91 | }); 92 | break; 93 | } 94 | } 95 | } 96 | } 97 | 98 | async attach({ tabId, url }) { 99 | if (url === '' || url.startsWith('http://') || url.startsWith('https://')) { 100 | if (this.debuggers.some(d => d.tabId === tabId)) { 101 | // already attached 102 | } else { 103 | const debugger_ = new Debugger(tabId); 104 | try { 105 | await debugger_.attach(); 106 | console.debug('attached', { tabId }); 107 | } catch (e) { 108 | console.error(e); 109 | return; 110 | } 111 | await this.setInterceptor(debugger_); 112 | this.debuggers.push(debugger_); 113 | chrome.tabs.executeScript(tabId, { file: 'content-script.js', allFrames: true }, () => { 114 | if (chrome.runtime.lastError && chrome.runtime.lastError.message) { 115 | // console.debug(chrome.runtime.lastError.message); 116 | } 117 | }); 118 | } 119 | } 120 | }; 121 | 122 | async stop() { 123 | this.isRunning = false; 124 | for (const debugger_ of this.debuggers) { 125 | await debugger_.sendCommand('Network.clearBrowserCache'); 126 | await debugger_.detach(); 127 | console.debug('detached', { tabId: debugger_.tabId }); 128 | } 129 | this.debuggers = []; 130 | this.sourceMaps = {}; 131 | this.jsCache = {}; 132 | this.bodyMaps = {}; 133 | chrome.tabs.onCreated.removeListener(this.boundOnCreatedHandler); 134 | chrome.webNavigation.onBeforeNavigate.removeListener(this.boundOnBeforeNavigateHandler); 135 | chrome.webNavigation.onDOMContentLoaded.removeListener(this.boundOnDOMContentLoadedHandler); 136 | chrome.debugger.onDetach.removeListener(this.boundOnDetachHandler); 137 | } 138 | 139 | /** 140 | * set interception 141 | * @param debugger_ {Debugger} 142 | * @returns {Promise} 143 | */ 144 | async setInterceptor(debugger_) { 145 | await debugger_.sendCommand('Page.enable'); 146 | await debugger_.sendCommand('Page.addScriptToEvaluateOnNewDocument', { source: PRELOAD_SOURCE }); 147 | 148 | await debugger_.sendCommand('Network.enable'); 149 | await debugger_.sendCommand('Network.setRequestInterception', { 150 | patterns: [ 151 | { 152 | urlPattern: '*', 153 | resourceType: 'Document', 154 | interceptionStage: 'HeadersReceived' 155 | }, 156 | { 157 | urlPattern: '*', 158 | resourceType: 'Script', 159 | interceptionStage: 'HeadersReceived' 160 | } 161 | 162 | ], 163 | }); 164 | await debugger_.sendCommand('Network.clearBrowserCache'); 165 | 166 | await debugger_.on('Network.requestIntercepted', async({ interceptionId, resourceType, responseStatusCode, responseHeaders, request }) => { 167 | if (responseStatusCode === 200 && ['Document', 'Script'].includes(resourceType) 168 | && (request.url.startsWith('http://') || request.url.startsWith('https://'))) { 169 | // OK 170 | } else { 171 | await debugger_.sendCommand('Network.continueInterceptedRequest', { interceptionId }); 172 | return; 173 | } 174 | if (resourceType === 'Script' && this.jsCache[request.url]) { 175 | // console.debug('cache hit', request.url); 176 | const { start, end, map, rawResponse, body } = this.jsCache[request.url]; 177 | this.setSourceMap(debugger_.tabId, request.url, start, end, map); 178 | this.setBodyMap(debugger_.tabId, request.url, body); 179 | await debugger_.sendCommand('Network.continueInterceptedRequest', { 180 | interceptionId, 181 | rawResponse, 182 | }); 183 | return; 184 | } 185 | 186 | let interceptTime = Date.now(); 187 | const { body, base64Encoded } = await debugger_.sendCommand( 188 | 'Network.getResponseBodyForInterception', 189 | { interceptionId }, 190 | ); 191 | const headerLines = []; 192 | for (const key of Object.keys(responseHeaders)) { 193 | if (key.toLowerCase() === 'content-type') { 194 | if (responseHeaders[key].toLowerCase().includes('text') || responseHeaders[key].toLowerCase().includes('javascript')) { 195 | // OK 196 | } else { 197 | // not text 198 | await debugger_.sendCommand('Network.continueInterceptedRequest', { interceptionId }); 199 | return; 200 | } 201 | } 202 | headerLines.push(`${key}: ${responseHeaders[key]}`); 203 | } 204 | let originalBodyStr; 205 | if (base64Encoded) { 206 | // assume utf8 207 | originalBodyStr = Buffer.from(body, 'base64').toString(); 208 | } else { 209 | originalBodyStr = body; 210 | } 211 | 212 | let encoding = null; 213 | if (base64Encoded) { 214 | for (const key of Object.keys(responseHeaders)) { 215 | const value = responseHeaders[key]; 216 | if (key.toLowerCase() === 'content-type' && value.includes('charset=')) { 217 | const m = value.match(/charset=['"]?([\w-]+)/); 218 | if (m) { 219 | encoding = m[1].trim(); 220 | // console.debug('encoding', encoding); 221 | } 222 | } 223 | } 224 | if (resourceType === 'Document') { 225 | if (originalBodyStr.includes(`charset=`)) { 226 | const m = originalBodyStr.match(/charset=['"]?([\w-]+)/); 227 | if (m) { 228 | encoding = m[1].trim(); 229 | // console.debug('encoding', encoding); 230 | } 231 | } 232 | } 233 | if (!encoding) { 234 | // auto-detect 235 | encoding = Encoding.detect(Buffer.from(body, 'base64')); 236 | // console.debug('encoding', encoding); 237 | } 238 | if (encoding) { 239 | originalBodyStr = iconv.decode(Buffer.from(body, 'base64'), encoding); 240 | } 241 | } 242 | 243 | // console.debug('originalBodyStr', originalBodyStr); 244 | let newBodyStr = null; 245 | let start = null; 246 | let end = null; 247 | let map = null; 248 | let convertTime = Date.now(); 249 | if (resourceType === 'Document') { 250 | newBodyStr = originalBodyStr; 251 | const scriptTagStrs = originalBodyStr.match(/]*?>[\s\S]+?<\/script>/ig); 252 | for (const scriptTagStr of scriptTagStrs || []) { 253 | const originalCode = scriptTagStr.match(/]*?>(?:\s*\s*)?<\/script>/)[1]; 254 | const converted = convert(originalCode); 255 | const code = converted.code; 256 | start = newBodyStr.indexOf(originalCode); 257 | end = start + code.length + 1; 258 | map = converted.map; 259 | newBodyStr = newBodyStr.replace(originalCode, code); 260 | this.setSourceMap(debugger_.tabId, request.url, start, end, map); 261 | } 262 | } else if (resourceType === 'Script') { 263 | const converted = convert(originalBodyStr); 264 | const code = converted.code; 265 | newBodyStr = code; 266 | start = 0; 267 | end = code.length + 1; 268 | map = converted.map; 269 | this.setSourceMap(debugger_.tabId, request.url, start, end, map); 270 | } else { 271 | throw new Error(); 272 | } 273 | // console.debug('newBodyStr', newBodyStr); 274 | convertTime = Date.now() - convertTime; 275 | console.debug(request.url, 'convert', `${convertTime} ms`); 276 | 277 | let rawResponse; 278 | if (encoding) { 279 | const bodyBuf = iconv.encode(newBodyStr, encoding); 280 | rawResponse = Buffer.concat([Buffer.from(`HTTP/1.1 200 OK\r\n${headerLines.join('\r\n')}\r\n\r\n`), bodyBuf]).toString('base64'); 281 | } else { 282 | rawResponse = Buffer.from(`HTTP/1.1 200 OK\r\n${headerLines.join('\r\n')}\r\n\r\n${newBodyStr}`).toString('base64'); 283 | } 284 | 285 | if (resourceType === 'Script') { 286 | this.jsCache[request.url] = { start, end, map, rawResponse, body: newBodyStr }; 287 | setTimeout(() => delete this.jsCache[request.url], 1000 * 60 * 60 * 24); 288 | } 289 | this.setBodyMap(debugger_.tabId, request.url, newBodyStr); 290 | 291 | await debugger_.sendCommand('Network.continueInterceptedRequest', { 292 | interceptionId, 293 | rawResponse, 294 | }); 295 | interceptTime = Date.now() - interceptTime; 296 | console.debug(request.url, 'intercept', `${interceptTime} ms`); 297 | }); 298 | } 299 | 300 | setSourceMap(tabId, url, start, end, map) { 301 | const key = JSON.stringify({ tabId, url, start, end }); 302 | this.sourceMaps[key] = map; 303 | } 304 | 305 | setBodyMap(tabId, url, body) { 306 | const key = JSON.stringify({ tabId, url }); 307 | this.bodyMaps[key] = body; 308 | } 309 | }; -------------------------------------------------------------------------------- /src/background/index.js: -------------------------------------------------------------------------------- 1 | import actions from '../models/extension-ui-actions'; 2 | import Interceptor from './interceptor'; 3 | import PocChecker from './poc-checker'; 4 | import {name} from '../../package'; 5 | import sourceMap from 'source-map'; 6 | 7 | sourceMap.SourceMapConsumer.initialize({ 8 | "lib/mappings.wasm": "https://unpkg.com/source-map@0.7.3/lib/mappings.wasm" 9 | }); 10 | 11 | const KEYWORD = '11111111'; 12 | 13 | class Background { 14 | constructor() { 15 | this.interceptor = new Interceptor(); 16 | 17 | chrome.extension.onConnect.addListener(port => { 18 | if (port.name === name) { 19 | console.debug('port connected'); 20 | port.onMessage.addListener(msg => { 21 | console.debug('port message received', msg); 22 | if (msg && msg.action) { 23 | if (msg.action === actions.IS_RUNNING) { 24 | port.postMessage({ isRunning: this.isRunning() }); 25 | console.debug('isRunning', this.isRunning()); 26 | } else if (msg.action === actions.START) { 27 | this.start(); 28 | } else if (msg.action === actions.STOP) { 29 | this.stop(); 30 | } else if (msg.action === actions.REMOVE) { 31 | this.remove(msg); 32 | } else if (msg.action === actions.REMOVE_ALL) { 33 | this.removeAll(); 34 | } 35 | } 36 | }); 37 | } 38 | }); 39 | chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { 40 | console.debug('message received', msg); 41 | if (msg.action === actions.ADD_ALL) { 42 | msg.tabId = sender.tab.id; 43 | this.addAll(msg); 44 | } else if (msg.action === actions.REMOVE) { 45 | this.remove(msg); 46 | } else if (msg.action === actions.CHECK_AND_GENERATE_POC) { 47 | this.checkAndGeneratePoc(msg); 48 | } 49 | sendResponse(); 50 | }); 51 | } 52 | 53 | async checkAndGeneratePoc({ resultId }) { 54 | console.debug('checkAndGeneratePoc', resultId); 55 | const isRunning = this.isRunning(); 56 | if (isRunning) { 57 | await this.stop(); 58 | } 59 | const result = await this.getResult(resultId); 60 | result.pocUrl = null; 61 | console.debug('result', result); 62 | const urls = this.getPocUrls(result); 63 | for (const url of urls) { 64 | const detect = await this.checkOne(url); 65 | if (detect) { 66 | result.pocUrl = url; 67 | break; 68 | } 69 | } 70 | await this.updateResult(result); 71 | if (isRunning) { 72 | await this.start(); 73 | } 74 | } 75 | 76 | updateResult(result) { 77 | return new Promise(resolve => { 78 | chrome.storage.local.get(['results'], async items => { 79 | const results = items.results || []; 80 | results.forEach(r => { 81 | if (r.id === result.id) { 82 | Object.assign(r, result); 83 | } 84 | }); 85 | chrome.storage.local.set({ results }, async() => { 86 | chrome.runtime.sendMessage({ action: actions.SET_POC, resultId: result.id, pocUrl: result.pocUrl }); 87 | resolve(); 88 | }); 89 | }); 90 | }); 91 | } 92 | 93 | getPocUrls({ url, source: { label } }) { 94 | const urls = []; 95 | 96 | const prefixes = [`javascript://alert(${KEYWORD})//`, `//katagaitai.net/${KEYWORD}/`]; 97 | const suffixes = [`'-alert(${KEYWORD})-'`, `"-alert(${KEYWORD})-"`, `-alert(${KEYWORD})-`, `'">`]; 98 | 99 | if (label.includes('referrer')) { 100 | for (const suffix of suffixes) { 101 | urls.push(`https://katagaitai.net/302.php?src=${encodeURIComponent(url)}&${suffix}`); 102 | } 103 | } else { 104 | const o = new URL(url); 105 | let query = o.search.slice(1); 106 | if (!query) { 107 | o.search = 'dummy=dummy'; 108 | } 109 | query = o.search.slice(1); 110 | if (query) { 111 | const namedValues = query.split('&'); 112 | const params = []; 113 | for (const namedValue of namedValues) { 114 | const elems = namedValue.split('='); 115 | const name = elems[0]; 116 | const value = elems.slice(1).join('='); 117 | params.push({ name, value }); 118 | } 119 | for (let i = 0; i < params.length; i++) { 120 | const { name, value } = params[i]; 121 | for (const prefix of prefixes) { 122 | const newParams = JSON.parse(JSON.stringify(params)); 123 | newParams[i] = { name, value: prefix + value }; 124 | const o2 = new URL(url); 125 | o2.search = newParams.map(({ name, value }) => `${name}=${value}`).join('&'); 126 | urls.push(o2.toString()); 127 | } 128 | for (const suffix of suffixes) { 129 | const newParams = JSON.parse(JSON.stringify(params)); 130 | newParams[i] = { name, value: value + suffix }; 131 | const o2 = new URL(url); 132 | o2.search = newParams.map(({ name, value }) => `${name}=${value}`).join('&'); 133 | urls.push(o2.toString()); 134 | } 135 | } 136 | } 137 | let hash = o.hash.slice(1); 138 | if (!hash) { 139 | o.hash = 'dummy'; 140 | } 141 | hash = o.hash.slice(1); 142 | if (hash) { 143 | for (const prefix of prefixes) { 144 | const o2 = new URL(url); 145 | o2.hash = prefix + hash; 146 | urls.push(o2.toString()); 147 | } 148 | for (const suffix of suffixes) { 149 | const o2 = new URL(url); 150 | o2.hash = hash + suffix; 151 | urls.push(o2.toString()); 152 | } 153 | } 154 | } 155 | console.debug('urls', urls); 156 | return urls; 157 | } 158 | 159 | checkOne(url) { 160 | return new Promise(resolve => { 161 | console.debug('checkOne', url); 162 | chrome.windows.create({ url: 'about:blank' }, async window => { 163 | const tab = window.tabs[0]; 164 | if (tab) { 165 | const checker = new PocChecker(window.id, tab.id, url, KEYWORD); 166 | await checker.start(); 167 | const timeoutTimer = setTimeout(() => { 168 | console.debug('timeout', window.id); 169 | checker.setIdle(); 170 | }, 5000); 171 | const checkTimer = setInterval(async() => { 172 | if (checker.isIdle()) { 173 | console.debug('idle', window.id); 174 | await checker.stop(); 175 | clearTimeout(timeoutTimer); 176 | clearInterval(checkTimer); 177 | console.debug('checkOne done', url, checker.detect); 178 | resolve(checker.detect); 179 | } 180 | }, 100); 181 | } 182 | }); 183 | }); 184 | } 185 | 186 | getResult(resultId) { 187 | return new Promise(resolve => { 188 | chrome.storage.local.get('results', async items => { 189 | const results = items.results || []; 190 | resolve(results.find(r => r.id === Number(resultId))); 191 | }); 192 | }); 193 | } 194 | 195 | isRunning() { 196 | return this.interceptor.isRunning; 197 | } 198 | 199 | async start() { 200 | await this.interceptor.start(); 201 | chrome.browserAction.setIcon({ path: './images/icon-green.png' }); 202 | chrome.browserAction.setBadgeBackgroundColor({ color: '#FF0000' }); 203 | await this.setCountBadge(); 204 | console.debug('start'); 205 | } 206 | 207 | async stop() { 208 | await this.interceptor.stop(); 209 | chrome.browserAction.setIcon({ path: './images/icon-black.png' }); 210 | chrome.browserAction.setBadgeBackgroundColor({ color: '#45C8F1' }); 211 | await this.setCountBadge(); 212 | console.debug('stop'); 213 | } 214 | 215 | setCountBadge() { 216 | return new Promise(resolve => { 217 | chrome.storage.local.get('results', items => { 218 | const results = items.results || []; 219 | // console.debug('results', results); 220 | const count = Object.keys(results).length; 221 | if (count > 0) { 222 | chrome.browserAction.setBadgeText({ text: String(count) }); 223 | } else { 224 | chrome.browserAction.setBadgeText({ text: this.isRunning() ? 'ON' : '' }); 225 | } 226 | resolve(); 227 | }); 228 | }); 229 | } 230 | 231 | remove({ resultId }) { 232 | return new Promise(resolve => { 233 | chrome.storage.local.get('results', async items => { 234 | let results = items.results || []; 235 | results = results.filter(r => r.id !== Number(resultId)); 236 | chrome.storage.local.set({ results }, async() => { 237 | console.debug('remove', resultId); 238 | await this.setCountBadge(); 239 | resolve(); 240 | }); 241 | }); 242 | }); 243 | } 244 | 245 | removeAll() { 246 | return new Promise(resolve => { 247 | chrome.storage.local.remove('results', async() => { 248 | console.debug('removeAll'); 249 | await this.setCountBadge(); 250 | resolve(); 251 | }); 252 | }); 253 | } 254 | 255 | addAll({ tabId, results }) { 256 | return new Promise(resolve => { 257 | chrome.storage.local.get(['results', 'nextId'], async items => { 258 | const oldResults = items.results || []; 259 | let nextId = items.nextId || 1; 260 | for (const result of results) { 261 | result.id = nextId++; 262 | await this.setOriginalStacktrace({ tabId, result }); 263 | } 264 | 265 | // remove same results 266 | function toJSON(r) { 267 | return JSON.stringify({ 268 | url: r.url, 269 | source: { 270 | label: r.source.label, 271 | stacktrace: r.source.stacktrace.map(t => ({ url: t.url, line: t.line, column: t.column })) 272 | }, 273 | sink: { 274 | label: r.sink.label, 275 | stacktrace: r.sink.stacktrace.map(t => ({ url: t.url, line: t.line, column: t.column })) 276 | }, 277 | }); 278 | } 279 | 280 | const oldJsons = oldResults.map(r => toJSON(r)); 281 | console.debug('oldResults', oldResults); 282 | const diffResults = results.filter(r => !oldJsons.includes(toJSON(r))); 283 | console.debug('diffResults', diffResults); 284 | const newResults = [...oldResults, ...diffResults]; 285 | chrome.storage.local.set({ results: newResults, nextId }, async() => { 286 | console.debug('newResults', newResults, 'nextId', nextId); 287 | await this.setCountBadge(); 288 | resolve(); 289 | }); 290 | }); 291 | }); 292 | } 293 | 294 | setOriginalStacktrace({ tabId, result }) { 295 | return new Promise(async resolve => { 296 | await this.resolveStackTrace(result.source.stacktrace, tabId); 297 | await this.resolveStackTrace(result.sink.stacktrace, tabId); 298 | resolve(); 299 | }); 300 | } 301 | 302 | async resolveStackTrace(stacktrace, tabId) { 303 | const sourceMaps = this.interceptor.sourceMaps; 304 | const bodyMaps = this.interceptor.bodyMaps; 305 | for (let i = 0; i < stacktrace.length; i++) { 306 | const trace = stacktrace[i]; 307 | // console.debug('trace', trace); 308 | const { url, line, column } = trace; 309 | const body = bodyMaps[JSON.stringify({ tabId, url })]; 310 | if (body) { 311 | const offset = body.match(new RegExp(`^([^\n]*\n){${line - 1}}[^\n]{${column - 1}}`))[0].length; 312 | if (offset) { 313 | for (const key of Object.keys(sourceMaps)) { 314 | const sourceMapMeta = JSON.parse(key); 315 | if (sourceMapMeta.tabId === tabId && sourceMapMeta.url === url && sourceMapMeta.start <= offset && offset < sourceMapMeta.end) { 316 | const map = sourceMaps[key]; 317 | // console.debug('resolveStackTrace', key, map); 318 | stacktrace[i] = await this.getOriginalTrace(trace, map, body.slice(0, sourceMapMeta.start).split('\n').length); 319 | break; 320 | } 321 | } 322 | } else { 323 | console.debug('no offset', { trace, body }); 324 | stacktrace[i] = { url, line: 0, column: 0, code: 'unknown' }; 325 | } 326 | } else { 327 | console.debug('no body', { trace }); 328 | stacktrace[i] = { url, line: 0, column: 0, code: 'unknown' }; 329 | } 330 | } 331 | } 332 | 333 | async getOriginalTrace(trace, map, startLine) { 334 | const { url, line, column } = trace; 335 | const consumer = await new sourceMap.SourceMapConsumer(map); 336 | try { 337 | // column start from 1 338 | // Position.column start from 0 339 | const pos = consumer.originalPositionFor({ line: line - startLine + 1, column: column - 1 }); 340 | // console.debug('pos', pos); 341 | let code = map.sourcesContent[0].split('\n')[pos.line - 1].slice(pos.column); 342 | if (code.length > 255) { 343 | code = code.slice(0, 255) + '...'; 344 | } 345 | return { url, line: pos.line + startLine - 1, column: pos.column + 1, code }; 346 | } finally { 347 | consumer.destroy(); 348 | } 349 | } 350 | } 351 | 352 | //mock 353 | // import Result from '../models/result'; 354 | // import Source from "../models/source"; 355 | // import Sink from "../models/sink"; 356 | // const urls = [ 357 | // 'http://scan.example.com:3333/shop?keyword=aaa', 358 | // 'http://scan.example.com:3333/shop?keyword=aaa', 359 | // 'http://scan.example.com:3333/shop?keyword=aaa', 360 | // 'http://scan.example.com:3333/shop?keyword=aaa', 361 | // ]; 362 | // const results = []; 363 | // for (let i = 0; i < urls.length; i++) { 364 | // const result = new Result({ 365 | // id: i + 1, 366 | // url: urls[i], 367 | // source: new Source({ 368 | // label: 'window.location.href', 369 | // stacktrace: ['http://scan.example.com:3333/shop?keyword=aaa:490:253', 'http://scan.example.com:3333/shop?keyword=aaa:409:1'] 370 | // }), 371 | // sink: new Sink({ 372 | // label: 'Element.innerHTML', 373 | // stacktrace: ['http://scan.example.com:3333/shop?keyword=aaa:490:5', 'http://scan.example.com:3333/shop?keyword=aaa:409:1'] 374 | // }) 375 | // }); 376 | // results.push(result); 377 | // } 378 | // chrome.storage.local.set({ results, nextId: 5 }, () => { 379 | // console.debug('mock added'); 380 | // }); 381 | 382 | (async() => { 383 | const bg = new Background(); 384 | await bg.setCountBadge(); 385 | window.__dombasedxssfinder_background = bg; 386 | })(); 387 | -------------------------------------------------------------------------------- /src/background/preload.js: -------------------------------------------------------------------------------- 1 | if (!window.__dombasedxssfinder_preload && (location.href.startsWith('http://') || location.href.startsWith('https://'))) { 2 | window.__dombasedxssfinder_preload = true; 3 | 4 | (function() { 5 | /////////////////////////////////////////////// 6 | // String.prototype 7 | /////////////////////////////////////////////// 8 | 9 | const stringPrototypeAnchor = String.prototype.anchor; 10 | String.prototype.anchor = function() { 11 | if (__is_dombasedxssfinder_string(this)) { 12 | const str = stringPrototypeAnchor.apply(this.toString(), arguments); 13 | return new __dombasedxssfinder_String(str, this); 14 | } 15 | return stringPrototypeAnchor.apply(this, arguments); 16 | }; 17 | 18 | const stringPrototypeBig = String.prototype.big; 19 | String.prototype.big = function() { 20 | if (__is_dombasedxssfinder_string(this)) { 21 | const str = stringPrototypeBig.apply(this.toString(), arguments); 22 | return new __dombasedxssfinder_String(str, this); 23 | } 24 | return stringPrototypeBig.apply(this, arguments); 25 | }; 26 | 27 | const stringPrototypeBlink = String.prototype.blink; 28 | String.prototype.blink = function() { 29 | if (__is_dombasedxssfinder_string(this)) { 30 | const str = stringPrototypeBlink.apply(this.toString(), arguments); 31 | return new __dombasedxssfinder_String(str, this); 32 | } 33 | return stringPrototypeBlink.apply(this, arguments); 34 | }; 35 | 36 | const stringPrototypeBold = String.prototype.bold; 37 | String.prototype.bold = function() { 38 | if (__is_dombasedxssfinder_string(this)) { 39 | const str = stringPrototypeBold.apply(this.toString(), arguments); 40 | return new __dombasedxssfinder_String(str, this); 41 | } 42 | return stringPrototypeBold.apply(this, arguments); 43 | }; 44 | 45 | const stringPrototypeCharAt = String.prototype.charAt; 46 | String.prototype.charAt = function() { 47 | if (__is_dombasedxssfinder_string(this)) { 48 | const str = stringPrototypeCharAt.apply(this.toString(), arguments); 49 | return new __dombasedxssfinder_String(str, this); 50 | } 51 | return stringPrototypeCharAt.apply(this, arguments); 52 | }; 53 | 54 | const stringPrototypeCharCodeAt = String.prototype.charCodeAt; 55 | String.prototype.charCodeAt = function() { 56 | return stringPrototypeCharCodeAt.apply(this.toString(), arguments); 57 | }; 58 | 59 | const stringPrototypeCodePointAt = String.prototype.codePointAt; 60 | String.prototype.codePointAt = function() { 61 | return stringPrototypeCodePointAt.apply(this.toString(), arguments); 62 | }; 63 | 64 | const stringPrototypeConcat = String.prototype.concat; 65 | String.prototype.concat = function() { 66 | const sources = []; 67 | for (let i = 0; i < arguments.length; i++) { 68 | arguments[i] = __convert_to_dombasedxssfinder_string_if_location(arguments[i]); 69 | if (__is_dombasedxssfinder_string(arguments[i])) { 70 | arguments[i].sources.forEach(e => sources.push(e)); 71 | } 72 | } 73 | if (__is_dombasedxssfinder_string(this)) { 74 | this.sources.forEach(e => sources.push(e)); 75 | } 76 | if (sources.size > 0) { 77 | const str = stringPrototypeConcat.apply(this.toString(), arguments); 78 | return new __dombasedxssfinder_String(str, { sources }); 79 | } 80 | return stringPrototypeConcat.apply(this, arguments); 81 | }; 82 | 83 | const stringPrototypeEndsWith = String.prototype.endsWith; 84 | String.prototype.endsWith = function() { 85 | return stringPrototypeEndsWith.apply(this.toString(), arguments); 86 | }; 87 | 88 | const stringPrototypeFixed = String.prototype.fixed; 89 | String.prototype.fixed = function() { 90 | if (__is_dombasedxssfinder_string(this)) { 91 | const str = stringPrototypeFixed.apply(this.toString(), arguments); 92 | return new __dombasedxssfinder_String(str, this); 93 | } 94 | return stringPrototypeFixed.apply(this, arguments); 95 | }; 96 | 97 | const stringPrototypeFontcolor = String.prototype.fontcolor; 98 | String.prototype.fontcolor = function() { 99 | if (__is_dombasedxssfinder_string(this)) { 100 | const str = stringPrototypeFontcolor.apply(this.toString(), arguments); 101 | return new __dombasedxssfinder_String(str, this); 102 | } 103 | return stringPrototypeFontcolor.apply(this, arguments); 104 | }; 105 | 106 | const stringPrototypeFontsize = String.prototype.fontsize; 107 | String.prototype.fontsize = function() { 108 | if (__is_dombasedxssfinder_string(this)) { 109 | const str = stringPrototypeFontsize.apply(this.toString(), arguments); 110 | return new __dombasedxssfinder_String(str, this); 111 | } 112 | return stringPrototypeFontsize.apply(this, arguments); 113 | }; 114 | 115 | const stringPrototypeIncludes = String.prototype.includes; 116 | String.prototype.includes = function() { 117 | return stringPrototypeIncludes.apply(this.toString(), arguments); 118 | }; 119 | 120 | const stringPrototypeIndexOf = String.prototype.indexOf; 121 | String.prototype.indexOf = function() { 122 | return stringPrototypeIndexOf.apply(this.toString(), arguments); 123 | }; 124 | 125 | const stringPrototypeItalics = String.prototype.italics; 126 | String.prototype.italics = function() { 127 | if (__is_dombasedxssfinder_string(this)) { 128 | const str = stringPrototypeItalics.apply(this.toString(), arguments); 129 | return new __dombasedxssfinder_String(str, this); 130 | } 131 | return stringPrototypeItalics.apply(this, arguments); 132 | }; 133 | 134 | const stringPrototypeLastIndexOf = String.prototype.lastIndexOf; 135 | String.prototype.lastIndexOf = function() { 136 | return stringPrototypeLastIndexOf.apply(this.toString(), arguments); 137 | }; 138 | 139 | const stringPrototypeLink = String.prototype.link; 140 | String.prototype.link = function() { 141 | if (__is_dombasedxssfinder_string(this)) { 142 | const str = stringPrototypeLink.apply(this.toString(), arguments); 143 | return new __dombasedxssfinder_String(str, this); 144 | } 145 | return stringPrototypeLink.apply(this, arguments); 146 | }; 147 | 148 | const stringPrototypeLocaleCompare = String.prototype.localeCompare; 149 | String.prototype.localeCompare = function() { 150 | return stringPrototypeLocaleCompare.apply(this.toString(), arguments); 151 | }; 152 | 153 | const stringPrototypeMatch = String.prototype.match; 154 | // TODO propagate taints of the regexp argument 155 | String.prototype.match = function() { 156 | if (__is_dombasedxssfinder_string(this)) { 157 | const array = stringPrototypeMatch.apply(this.toString(), arguments); 158 | if (array === null) { 159 | return null; 160 | } 161 | for (let i = 0; i < array.length; i++) { 162 | array[i] = new __dombasedxssfinder_String(array[i], this); 163 | } 164 | return array; 165 | } 166 | return stringPrototypeMatch.apply(this, arguments); 167 | }; 168 | 169 | const stringPrototypeMatchAll = String.prototype.matchAll; 170 | // TODO propagate taints of the regexp argument 171 | String.prototype.matchAll = function() { 172 | if (__is_dombasedxssfinder_string(this)) { 173 | const iterator = stringPrototypeMatchAll.apply(this.toString(), arguments); 174 | return function* () { 175 | for (const array of iterator) { 176 | for (let i = 0; i < array.length; i++) { 177 | array[i] = new __dombasedxssfinder_String(array[i], this); 178 | } 179 | yield array; 180 | } 181 | }; 182 | } 183 | return stringPrototypeMatchAll.apply(this, arguments); 184 | }; 185 | 186 | const stringPrototypeNormalize = String.prototype.normalize; 187 | String.prototype.normalize = function() { 188 | if (__is_dombasedxssfinder_string(this)) { 189 | const str = stringPrototypeNormalize.apply(this.toString(), arguments); 190 | return new __dombasedxssfinder_String(str, this); 191 | } 192 | return stringPrototypeNormalize.apply(this, arguments); 193 | }; 194 | 195 | const stringPrototypePadEnd = String.prototype.padEnd; 196 | String.prototype.padEnd = function() { 197 | const sources = []; 198 | arguments[1] = __convert_to_dombasedxssfinder_string_if_location(arguments[1]); 199 | if (__is_dombasedxssfinder_string(arguments[1])) { 200 | arguments[1].sources.forEach(e => sources.push(e)); 201 | } 202 | if (__is_dombasedxssfinder_string(this)) { 203 | this.sources.forEach(e => sources.push(e)); 204 | } 205 | if (sources.size > 0) { 206 | const str = stringPrototypePadEnd.apply(this.toString(), arguments); 207 | return new __dombasedxssfinder_String(str, { sources }); 208 | } 209 | return stringPrototypePadEnd.apply(this, arguments); 210 | }; 211 | 212 | const stringPrototypePadStart = String.prototype.padStart; 213 | String.prototype.padStart = function() { 214 | const sources = []; 215 | arguments[1] = __convert_to_dombasedxssfinder_string_if_location(arguments[1]); 216 | if (__is_dombasedxssfinder_string(arguments[1])) { 217 | arguments[1].sources.forEach(e => sources.push(e)); 218 | } 219 | if (__is_dombasedxssfinder_string(this)) { 220 | this.sources.forEach(e => sources.push(e)); 221 | } 222 | if (sources.size > 0) { 223 | const str = stringPrototypePadStart.apply(this.toString(), arguments); 224 | return new __dombasedxssfinder_String(str, { sources }); 225 | } 226 | return stringPrototypePadStart.apply(this, arguments); 227 | }; 228 | 229 | const stringPrototypeRepeat = String.prototype.repeat; 230 | String.prototype.repeat = function() { 231 | if (__is_dombasedxssfinder_string(this)) { 232 | const str = stringPrototypeRepeat.apply(this.toString(), arguments); 233 | return new __dombasedxssfinder_String(str, this); 234 | } 235 | return stringPrototypeRepeat.apply(this, arguments); 236 | }; 237 | 238 | const stringPrototypeReplace = String.prototype.replace; 239 | String.prototype.replace = function() { 240 | const sources = []; 241 | arguments[1] = __convert_to_dombasedxssfinder_string_if_location(arguments[1]); 242 | if (__is_dombasedxssfinder_string(arguments[1])) { 243 | arguments[1].sources.forEach(e => sources.push(e)); 244 | } 245 | if (__is_dombasedxssfinder_string(this)) { 246 | this.sources.forEach(e => sources.push(e)); 247 | } 248 | if (sources.size > 0) { 249 | const str = stringPrototypeReplace.apply(this.toString(), arguments); 250 | return new __dombasedxssfinder_String(str, { sources }); 251 | } 252 | return stringPrototypeReplace.apply(this, arguments); 253 | }; 254 | 255 | const stringPrototypeSearch = String.prototype.search; 256 | String.prototype.search = function() { 257 | return stringPrototypeSearch.apply(this.toString(), arguments); 258 | }; 259 | 260 | const stringPrototypeSlice = String.prototype.slice; 261 | String.prototype.slice = function() { 262 | if (__is_dombasedxssfinder_string(this)) { 263 | const str = stringPrototypeSlice.apply(this.toString(), arguments); 264 | return new __dombasedxssfinder_String(str, this); 265 | } 266 | return stringPrototypeSlice.apply(this, arguments); 267 | }; 268 | 269 | const stringPrototypeSmall = String.prototype.small; 270 | String.prototype.small = function() { 271 | if (__is_dombasedxssfinder_string(this)) { 272 | const str = stringPrototypeSmall.apply(this.toString(), arguments); 273 | return new __dombasedxssfinder_String(str, this); 274 | } 275 | return stringPrototypeSlice.apply(this, arguments); 276 | }; 277 | 278 | const stringPrototypeSplit = String.prototype.split; 279 | String.prototype.split = function() { 280 | if (__is_dombasedxssfinder_string(this)) { 281 | const array = stringPrototypeSplit.apply(this.toString(), arguments); 282 | for (let i = 0; i < array.length; i++) { 283 | array[i] = new __dombasedxssfinder_String(array[i], this); 284 | } 285 | return array; 286 | } 287 | return stringPrototypeSplit.apply(this, arguments); 288 | }; 289 | 290 | const stringPrototypeStartsWith = String.prototype.startsWith; 291 | String.prototype.startsWith = function() { 292 | return stringPrototypeStartsWith.apply(this.toString(), arguments); 293 | }; 294 | 295 | const stringPrototypeStrike = String.prototype.strike; 296 | String.prototype.strike = function() { 297 | if (__is_dombasedxssfinder_string(this)) { 298 | const str = stringPrototypeStrike.apply(this.toString(), arguments); 299 | return new __dombasedxssfinder_String(str, this); 300 | } 301 | return stringPrototypeStrike.apply(this, arguments); 302 | }; 303 | 304 | const stringPrototypeSub = String.prototype.sub; 305 | String.prototype.sub = function() { 306 | if (__is_dombasedxssfinder_string(this)) { 307 | const str = stringPrototypeSub.apply(this.toString(), arguments); 308 | return new __dombasedxssfinder_String(str, this); 309 | } 310 | return stringPrototypeSub.apply(this, arguments); 311 | }; 312 | 313 | const stringPrototypeSubstr = String.prototype.substr; 314 | String.prototype.substr = function() { 315 | if (__is_dombasedxssfinder_string(this)) { 316 | const str = stringPrototypeSubstr.apply(this.toString(), arguments); 317 | return new __dombasedxssfinder_String(str, this); 318 | } 319 | return stringPrototypeSubstr.apply(this, arguments); 320 | }; 321 | 322 | const stringPrototypeSubstring = String.prototype.substring; 323 | String.prototype.substring = function() { 324 | if (__is_dombasedxssfinder_string(this)) { 325 | const str = stringPrototypeSubstring.apply(this.toString(), arguments); 326 | return new __dombasedxssfinder_String(str, this); 327 | } 328 | return stringPrototypeSubstring.apply(this, arguments); 329 | }; 330 | 331 | const stringPrototypeSup = String.prototype.sup; 332 | String.prototype.sup = function() { 333 | if (__is_dombasedxssfinder_string(this)) { 334 | const str = stringPrototypeSup.apply(this.toString(), arguments); 335 | return new __dombasedxssfinder_String(str, this); 336 | } 337 | return stringPrototypeSup.apply(this, arguments); 338 | }; 339 | 340 | const stringPrototypeToLocaleLowerCase = String.prototype.toLocaleLowerCase; 341 | String.prototype.toLocaleLowerCase = function() { 342 | if (__is_dombasedxssfinder_string(this)) { 343 | const str = stringPrototypeToLocaleLowerCase.apply(this.toString(), arguments); 344 | return new __dombasedxssfinder_String(str, this); 345 | } 346 | return stringPrototypeToLocaleLowerCase.apply(this, arguments); 347 | }; 348 | 349 | const stringPrototypeToLocaleUpperCase = String.prototype.toLocaleUpperCase; 350 | String.prototype.toLocaleUpperCase = function() { 351 | if (__is_dombasedxssfinder_string(this)) { 352 | const str = stringPrototypeToLocaleUpperCase.apply(this.toString(), arguments); 353 | return new __dombasedxssfinder_String(str, this); 354 | } 355 | return stringPrototypeToLocaleUpperCase.apply(this, arguments); 356 | }; 357 | 358 | const stringPrototypeToLowerCase = String.prototype.toLowerCase; 359 | String.prototype.toLowerCase = function() { 360 | if (__is_dombasedxssfinder_string(this)) { 361 | const str = stringPrototypeToLowerCase.apply(this.toString(), arguments); 362 | return new __dombasedxssfinder_String(str, this); 363 | } 364 | return stringPrototypeToLowerCase.apply(this, arguments); 365 | }; 366 | 367 | // skip String.prototype.toString, which is overwritten in __dombasedxssfinder_String 368 | 369 | const stringPrototypeToUpperCase = String.prototype.toUpperCase; 370 | String.prototype.toUpperCase = function() { 371 | if (__is_dombasedxssfinder_string(this)) { 372 | const str = stringPrototypeToUpperCase.apply(this.toString(), arguments); 373 | return new __dombasedxssfinder_String(str, this); 374 | } 375 | return stringPrototypeToUpperCase.apply(this, arguments); 376 | }; 377 | 378 | const stringPrototypeTrim = String.prototype.trim; 379 | String.prototype.trim = function() { 380 | if (__is_dombasedxssfinder_string(this)) { 381 | const str = stringPrototypeTrim.apply(this.toString(), arguments); 382 | return new __dombasedxssfinder_String(str, this); 383 | } 384 | return stringPrototypeTrim.apply(this, arguments); 385 | }; 386 | 387 | const stringPrototypeTrimEnd = String.prototype.trimEnd; 388 | String.prototype.trimEnd = function() { 389 | if (__is_dombasedxssfinder_string(this)) { 390 | const str = stringPrototypeTrimEnd.apply(this.toString(), arguments); 391 | return new __dombasedxssfinder_String(str, this); 392 | } 393 | return stringPrototypeTrimEnd.apply(this, arguments); 394 | }; 395 | 396 | const stringPrototypeTrimStart = String.prototype.trimStart; 397 | String.prototype.trimStart = function() { 398 | if (__is_dombasedxssfinder_string(this)) { 399 | const str = stringPrototypeTrimStart.apply(this.toString(), arguments); 400 | return new __dombasedxssfinder_String(str, this); 401 | } 402 | return stringPrototypeTrimStart.apply(this, arguments); 403 | }; 404 | 405 | // skip String.prototype.valueOf, which is overwritten in __dombasedxssfinder_String 406 | 407 | /////////////////////////////////////////////// 408 | // RegExp.prototype 409 | /////////////////////////////////////////////// 410 | 411 | const regExpPrototypeExec = RegExp.prototype.exec; 412 | RegExp.prototype.exec = function() { 413 | const array = regExpPrototypeExec.apply(this, arguments); 414 | if (array !== null && __is_dombasedxssfinder_string(arguments[0])) { 415 | for (let i = 0; i < array.length; i++) { 416 | array[i] = new __dombasedxssfinder_String(array[i], arguments[0]); 417 | } 418 | } 419 | return array; 420 | }; 421 | 422 | /////////////////////////////////////////////// 423 | // Range.prototype 424 | /////////////////////////////////////////////// 425 | const rangeCreateContextualFragment = Range.prototype.createContextualFragment; 426 | Range.prototype.createContextualFragment = function(fragment) { 427 | if (__is_dombasedxssfinder_string_html(fragment)) { 428 | __dombasedxssfinder_vulns_push(fragment.sources, 'Range.prototype.createContextualFragment()'); 429 | } 430 | return rangeCreateContextualFragment.apply(this, arguments); 431 | }; 432 | 433 | /////////////////////////////////////////////// 434 | // document 435 | /////////////////////////////////////////////// 436 | 437 | const documentWrite = document.write; 438 | document.write = function(...text) { 439 | for (let i = 0; i < text.length; i++) { 440 | if (__is_dombasedxssfinder_string_html(text[i])) { 441 | __dombasedxssfinder_vulns_push(text[i].sources, 'document.write()'); 442 | } 443 | } 444 | return documentWrite.apply(this, arguments); 445 | }; 446 | 447 | const documentWriteln = document.writeln; 448 | document.writeln = function(...text) { 449 | for (let i = 0; i < text.length; i++) { 450 | if (__is_dombasedxssfinder_string_html(text[i])) { 451 | __dombasedxssfinder_vulns_push(text[i].sources, 'document.writeln()'); 452 | } 453 | } 454 | return documentWriteln.apply(this, arguments); 455 | }; 456 | 457 | /////////////////////////////////////////////// 458 | // global functions 459 | /////////////////////////////////////////////// 460 | 461 | const _decodeURI = decodeURI; 462 | decodeURI = function(encodedURI) { 463 | encodedURI = __convert_to_dombasedxssfinder_string_if_location(encodedURI); 464 | if (__is_dombasedxssfinder_string(encodedURI)) { 465 | const str = _decodeURI.apply(this, [encodedURI.toString()]); 466 | const newStr = new __dombasedxssfinder_String(str, encodedURI); 467 | return newStr; 468 | } 469 | return _decodeURI.apply(this, arguments); 470 | }; 471 | 472 | const _encodeURI = encodeURI; 473 | encodeURI = function(URI) { 474 | URI = __convert_to_dombasedxssfinder_string_if_location(URI); 475 | if (__is_dombasedxssfinder_string(URI)) { 476 | const str = _encodeURI.apply(this, [URI.toString()]); 477 | const newStr = new __dombasedxssfinder_String(str, URI); 478 | return newStr; 479 | } 480 | return _encodeURI.apply(this, arguments); 481 | }; 482 | 483 | const _decodeURIComponent = decodeURIComponent; 484 | decodeURIComponent = function(encodedURI) { 485 | encodedURI = __convert_to_dombasedxssfinder_string_if_location(encodedURI); 486 | if (__is_dombasedxssfinder_string(encodedURI)) { 487 | const str = _decodeURIComponent.apply(this, [encodedURI.toString()]); 488 | const newStr = new __dombasedxssfinder_String(str, encodedURI); 489 | return newStr; 490 | } 491 | return _decodeURIComponent.apply(this, arguments); 492 | }; 493 | 494 | const _encodeURIComponent = encodeURIComponent; 495 | encodeURIComponent = function(URI) { 496 | URI = __convert_to_dombasedxssfinder_string_if_location(URI); 497 | if (__is_dombasedxssfinder_string(URI)) { 498 | const str = _encodeURIComponent.apply(this, [URI.toString()]); 499 | const newStr = new __dombasedxssfinder_String(str, URI); 500 | return newStr; 501 | } 502 | return _encodeURIComponent.apply(this, arguments); 503 | }; 504 | 505 | const _unescape = unescape; 506 | unescape = function(escapedString) { 507 | escapedString = __convert_to_dombasedxssfinder_string_if_location(escapedString); 508 | if (__is_dombasedxssfinder_string(escapedString)) { 509 | const str = _unescape.apply(this, [escapedString.toString()]); 510 | const newStr = new __dombasedxssfinder_String(str, escapedString); 511 | return newStr; 512 | } 513 | return _unescape.apply(this, arguments); 514 | }; 515 | 516 | const _escape = escape; 517 | escape = function(string) { 518 | string = __convert_to_dombasedxssfinder_string_if_location(string); 519 | if (__is_dombasedxssfinder_string(string)) { 520 | const str = _escape.apply(this, [string.toString()]); 521 | const newStr = new __dombasedxssfinder_String(str, string); 522 | return newStr; 523 | } 524 | return _escape.apply(this, arguments); 525 | }; 526 | 527 | const _eval = eval; 528 | eval = function(x) { 529 | if (__is_dombasedxssfinder_string_script(x)) { 530 | __dombasedxssfinder_vulns_push(x.sources, 'eval()'); 531 | // eval requires toString() 532 | return _eval.apply(this, [x.toString()]); 533 | } 534 | return _eval.apply(this, arguments); 535 | }; 536 | 537 | const _setInterval = setInterval; 538 | setInterval = function(handler) { 539 | if (__is_dombasedxssfinder_string_script(handler)) { 540 | __dombasedxssfinder_vulns_push(handler.sources, 'setTimeout()'); 541 | } 542 | return _setInterval.apply(this, arguments); 543 | }; 544 | 545 | const _setTimeout = setTimeout; 546 | setTimeout = function(handler) { 547 | if (__is_dombasedxssfinder_string_script(handler)) { 548 | __dombasedxssfinder_vulns_push(handler.sources, 'setTimeout()'); 549 | } 550 | return _setTimeout.apply(this, arguments); 551 | }; 552 | 553 | const _postMessage = postMessage; 554 | postMessage = function(message) { 555 | if (__is_dombasedxssfinder_string(message)) { 556 | arguments[0] = message.toString(); 557 | } 558 | return _postMessage.apply(this, arguments); 559 | }; 560 | })(); 561 | 562 | const __dombasedxssfinder_String = function(str, parent) { 563 | this.str = '' + str; 564 | this.sources = []; 565 | parent.sources.forEach(e => this.sources.push(e)); 566 | 567 | this.valueOf = function() { 568 | return this; 569 | }; 570 | 571 | this.toString = function() { 572 | return this.str; 573 | }; 574 | 575 | // str.length 576 | Object.defineProperty(this, 'length', { 577 | set: () => null, 578 | get: () => this.str.length 579 | }); 580 | 581 | // str[0] 582 | for (let i = 0; i < this.str.length; i++) { 583 | Object.defineProperty(this, i, { 584 | set: () => null, 585 | get: () => new __dombasedxssfinder_String(this.str[i], this) 586 | }); 587 | } 588 | 589 | Object.defineProperty(this, '__dombasedxssfinder_string', { 590 | set: () => null, 591 | get: () => true 592 | }); 593 | }; 594 | __dombasedxssfinder_String.prototype = String.prototype; 595 | 596 | function __dombasedxssfinder_plus(left, right) { 597 | left = __convert_to_dombasedxssfinder_string_if_location(left); 598 | right = __convert_to_dombasedxssfinder_string_if_location(right); 599 | if (__is_dombasedxssfinder_string(left) || __is_dombasedxssfinder_string(right)) { 600 | const sources = []; 601 | if (__is_dombasedxssfinder_string(left)) { 602 | left.sources.forEach(e => sources.push(e)); 603 | } 604 | if (__is_dombasedxssfinder_string(right)) { 605 | right.sources.forEach(e => sources.push(e)); 606 | } 607 | return new __dombasedxssfinder_String('' + left + right, { sources }); 608 | } 609 | try { 610 | return left + right; 611 | } catch (e) { 612 | return left.toString() + right.toString(); 613 | } 614 | } 615 | 616 | function __dombasedxssfinder_get(object, key) { 617 | // if (object === null || object === undefined) { 618 | // console.trace({object, key}); 619 | // } 620 | if (object === window.location) { 621 | if (key === 'hash') { 622 | return new __dombasedxssfinder_String(object[key], { 623 | sources: [__dombasedxssfinder_get_source('window.location.hash')], 624 | }); 625 | } else if (key === 'href') { 626 | return new __dombasedxssfinder_String(object[key], { 627 | sources: [__dombasedxssfinder_get_source('window.location.href')], 628 | }); 629 | } else if (key === 'pathname') { 630 | return new __dombasedxssfinder_String(object[key], { 631 | sources: [__dombasedxssfinder_get_source('window.location.pathname')], 632 | }); 633 | } else if (key === 'search') { 634 | return new __dombasedxssfinder_String(object[key], { 635 | sources: [__dombasedxssfinder_get_source('window.location.search')], 636 | }); 637 | } 638 | } else if (object === document) { 639 | if (key === 'documentURI') { 640 | return new __dombasedxssfinder_String(object[key], { 641 | sources: [__dombasedxssfinder_get_source('document.documentURI')], 642 | }); 643 | } else if (key === 'baseURI') { 644 | return new __dombasedxssfinder_String(object[key], { 645 | sources: [__dombasedxssfinder_get_source('document.baseURI')], 646 | }); 647 | } else if (key === 'URL') { 648 | return new __dombasedxssfinder_String(object[key], { 649 | sources: [__dombasedxssfinder_get_source('document.URL')], 650 | }); 651 | } else if (key === 'referrer' && object[key]) { 652 | return new __dombasedxssfinder_String(object[key], { 653 | sources: [__dombasedxssfinder_get_source('document.referrer')], 654 | }); 655 | } 656 | } 657 | return object[key]; 658 | } 659 | 660 | function __dombasedxssfinder_put(object, key, value) { 661 | // if (object === null || object === undefined) { 662 | // console.trace({object, key, value}); 663 | // } 664 | if (object[key] === window.location && __is_dombasedxssfinder_string_script(value)) { 665 | // __dombasedxssfinder_vulns_push(value.sources, 'window.location'); 666 | // kill navigation 667 | return; 668 | } else if (object === window.location && key === 'href' && __is_dombasedxssfinder_string_script(value) && value.toString() !== object[key]) { 669 | // __dombasedxssfinder_vulns_push(value.sources, 'window.location.href'); 670 | // kill navigation 671 | return; 672 | } else if (object instanceof Element && key === 'innerHTML' && __is_dombasedxssfinder_string_html(value)) { 673 | __dombasedxssfinder_vulns_push(value.sources, 'Element.innerHTML'); 674 | } else if (object instanceof Element && key === 'outerHTML' && __is_dombasedxssfinder_string_html(value)) { 675 | __dombasedxssfinder_vulns_push(value.sources, 'Element.outerHTML'); 676 | } else if (object instanceof HTMLScriptElement && key === 'src' && __is_dombasedxssfinder_string_url(value)) { 677 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLScriptElement.src'); 678 | } else if (object instanceof HTMLEmbedElement && key === 'src' && __is_dombasedxssfinder_string_url(value)) { 679 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLEmbedElement.src'); 680 | } else if (object instanceof HTMLIFrameElement && key === 'src' && __is_dombasedxssfinder_string_script(value)) { 681 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLIFrameElement.src'); 682 | } else if (object instanceof HTMLAnchorElement && key === 'href' && __is_dombasedxssfinder_string_script(value)) { 683 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLAnchorElement.href'); 684 | } else if (object instanceof HTMLFormElement && key === 'action' && __is_dombasedxssfinder_string_script(value)) { 685 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLFormElement.action'); 686 | } else if (object instanceof HTMLInputElement && key === 'formAction' && __is_dombasedxssfinder_string_script(value)) { 687 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLInputElement.formAction'); 688 | } else if (object instanceof HTMLButtonElement && key === 'formAction' && __is_dombasedxssfinder_string_script(value)) { 689 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLButtonElement.formAction'); 690 | } else if (object instanceof HTMLObjectElement && key === 'data' && __is_dombasedxssfinder_string_data_html(value)) { 691 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLObjectElement.data'); 692 | } else if (object instanceof HTMLScriptElement && key === 'text' && __is_dombasedxssfinder_string_script(value)) { 693 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLScriptElement.text'); 694 | } else if (object instanceof HTMLScriptElement && key === 'textContent' && __is_dombasedxssfinder_string_script(value)) { 695 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLScriptElement.textContent'); 696 | } else if (object instanceof HTMLScriptElement && key === 'innerText' && __is_dombasedxssfinder_string_script(value)) { 697 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLScriptElement.innerText'); 698 | } 699 | return object[key] = value; 700 | } 701 | 702 | 703 | function __dombasedxssfinder_new_Function() { 704 | const f = new Function(...arguments); 705 | if (__is_dombasedxssfinder_string_script(arguments[arguments.length - 1])) { 706 | __dombasedxssfinder_vulns_push(arguments[arguments.length - 1].sources, 'new Function()'); 707 | f.__dombasedxssfinder_str = arguments[arguments.length - 1]; 708 | } 709 | return f; 710 | } 711 | 712 | function __dombasedxssfinder_equal(left, right) { 713 | if (__is_dombasedxssfinder_string(left)) { 714 | left = left.toString(); 715 | } 716 | if (__is_dombasedxssfinder_string(right)) { 717 | right = right.toString(); 718 | } 719 | return left == right; 720 | } 721 | 722 | function __dombasedxssfinder_notEqual(left, right) { 723 | if (__is_dombasedxssfinder_string(left)) { 724 | left = left.toString(); 725 | } 726 | if (__is_dombasedxssfinder_string(right)) { 727 | right = right.toString(); 728 | } 729 | return left != right; 730 | } 731 | 732 | function __dombasedxssfinder_strictEqual(left, right) { 733 | if (__is_dombasedxssfinder_string(left)) { 734 | left = left.toString(); 735 | } 736 | if (__is_dombasedxssfinder_string(right)) { 737 | right = right.toString(); 738 | } 739 | return left === right; 740 | } 741 | 742 | function __dombasedxssfinder_strictNotEqual(left, right) { 743 | if (__is_dombasedxssfinder_string(left)) { 744 | left = left.toString(); 745 | } 746 | if (__is_dombasedxssfinder_string(right)) { 747 | right = right.toString(); 748 | } 749 | return left !== right; 750 | } 751 | 752 | function __dombasedxssfinder_typeof(o) { 753 | if (__is_dombasedxssfinder_string(o)) { 754 | return 'string'; 755 | } 756 | return typeof o; 757 | } 758 | 759 | function __is_dombasedxssfinder_string(o) { 760 | return o && o.__dombasedxssfinder_string; 761 | } 762 | 763 | function __is_dombasedxssfinder_string_html(o) { 764 | // 765 | o = __convert_to_dombasedxssfinder_string_if_location(o); 766 | return __is_dombasedxssfinder_string(o); 767 | } 768 | 769 | function __is_dombasedxssfinder_string_data_html(o) { 770 | // data:text/html, 771 | o = __convert_to_dombasedxssfinder_string_if_location(o); 772 | return __is_dombasedxssfinder_string(o); 773 | } 774 | 775 | function __is_dombasedxssfinder_string_script(o) { 776 | // alert() 777 | // javascript:alert() 778 | o = __convert_to_dombasedxssfinder_string_if_location(o); 779 | return __is_dombasedxssfinder_string(o); 780 | } 781 | 782 | function __is_dombasedxssfinder_string_url(o) { 783 | // //14.rs 784 | o = __convert_to_dombasedxssfinder_string_if_location(o); 785 | return __is_dombasedxssfinder_string(o); 786 | } 787 | 788 | function __dombasedxssfinder_property_call(object, key, ...arguments) { 789 | // if (object === null || object === undefined || typeof object[key] !== 'function') { 790 | // console.trace({object, key, arguments}); 791 | // } 792 | if (object[key] === window.location.assign) { 793 | // cannot overwrite, replace it when called. 794 | return (function(url) { 795 | if (__is_dombasedxssfinder_string_script(url)) { 796 | // __dombasedxssfinder_vulns_push(url.sources, 'window.location.assign()'); 797 | // kill navigation 798 | return; 799 | } 800 | }).apply(object, arguments); 801 | } else if (object[key] === window.location.replace) { 802 | // cannot overwrite, replace it when called. 803 | return (function(url) { 804 | if (__is_dombasedxssfinder_string_script(url)) { 805 | // __dombasedxssfinder_vulns_push(url.sources, 'window.location.replace()'); 806 | // kill navigation 807 | return; 808 | } 809 | }).apply(object, arguments); 810 | } else if (object instanceof Element && key === 'setAttribute') { 811 | const elementSetAttribute = object[key]; 812 | return (function(qualifiedName, value) { 813 | if (qualifiedName.startsWith('on') && __is_dombasedxssfinder_string_script(value)) { 814 | __dombasedxssfinder_vulns_push(value.sources, `Element.setAttribute('${qualifiedName}')`); 815 | } else if (this instanceof HTMLScriptElement && qualifiedName === 'src' && __is_dombasedxssfinder_string_url(value)) { 816 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLScriptElement.setAttribute(\'src\')'); 817 | } else if (this instanceof HTMLEmbedElement && qualifiedName === 'src' && __is_dombasedxssfinder_string_url(value)) { 818 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLEmbedElement.setAttribute(\'src\')'); 819 | } else if (this instanceof HTMLIFrameElement && qualifiedName === 'src' && __is_dombasedxssfinder_string_script(value)) { 820 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLIFrameElement.setAttribute(\'src\')'); 821 | } else if (this instanceof HTMLAnchorElement && qualifiedName === 'href' && __is_dombasedxssfinder_string_script(value)) { 822 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLAnchorElement.setAttribute(\'href\')'); 823 | } else if (this instanceof HTMLFormElement && qualifiedName === 'action' && __is_dombasedxssfinder_string_script(value)) { 824 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLFormElement.setAttribute(\'action\')'); 825 | } else if (this instanceof HTMLInputElement && qualifiedName === 'formaction' && __is_dombasedxssfinder_string_script(value)) { 826 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLInputElement.setAttribute(\'formaction\')'); 827 | } else if (this instanceof HTMLButtonElement && qualifiedName === 'formaction' && __is_dombasedxssfinder_string_script(value)) { 828 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLButtonElement.setAttribute(\'formaction\')'); 829 | } else if (this instanceof HTMLObjectElement && qualifiedName === 'data' && __is_dombasedxssfinder_string_data_html(value)) { 830 | __dombasedxssfinder_vulns_push(value.sources, 'HTMLObjectElement.setAttribute(\'data\')'); 831 | } 832 | elementSetAttribute.apply(this, arguments); 833 | }).apply(object, arguments); 834 | } else if (object instanceof Element && key === 'addEventListener') { 835 | const elementAddEventListener = object[key]; 836 | return (function(type, listener) { 837 | if (type === 'click' && listener && listener.__dombasedxssfinder_str && __is_dombasedxssfinder_string_script(listener.__dombasedxssfinder_str)) { 838 | __dombasedxssfinder_vulns_push(listener.__dombasedxssfinder_str.sources, 'Element.addEventListener(\'click\')'); 839 | } 840 | elementAddEventListener.apply(this, arguments); 841 | }).apply(object, arguments); 842 | } 843 | 844 | return object[key](...arguments); 845 | } 846 | 847 | function __dombasedxssfinder_call(func, ...arguments) { 848 | // if (typeof func !== 'function') { 849 | // console.trace({func, arguments}); 850 | // } 851 | if (func === window.location.assign) { 852 | // cannot overwrite, replace it when called. 853 | func = function(url) { 854 | if (__is_dombasedxssfinder_string_script(url)) { 855 | // __dombasedxssfinder_vulns_push(url.sources, 'window.location.assign()'); 856 | // kill navigation 857 | return; 858 | } 859 | }; 860 | } else if (func === window.location.replace) { 861 | // 上書きできないので呼び出し時に差し替える 862 | func = function(url) { 863 | if (__is_dombasedxssfinder_string_script(url)) { 864 | // __dombasedxssfinder_vulns_push(url.sources, 'window.location.replace()'); 865 | // kill navigation 866 | return; 867 | } 868 | }; 869 | } 870 | 871 | return func(...arguments); 872 | } 873 | 874 | function __convert_to_dombasedxssfinder_string_if_location(o) { 875 | if (o === window.location) { 876 | o = new __dombasedxssfinder_String(o.toString(), { 877 | sources: [__dombasedxssfinder_get_source('window.location')], 878 | }); 879 | } 880 | return o; 881 | } 882 | 883 | function __dombasedxssfinder_get_stacktrace() { 884 | const o = {}; 885 | Error.captureStackTrace(o); 886 | // console.debug(o.stack.replace(/^Error\n/, '').replace(/^\s+at\s+/mg, '')); 887 | const regExp = /(https?:\/\/\S+):(\d+):(\d+)/; 888 | return o.stack.replace(/^Error\n/, '').replace(/^\s+at\s+/mg, '').split('\n') 889 | .filter(e => regExp.test(e)) 890 | .map(e => { 891 | const m = e.match(regExp); 892 | const url = m[1]; 893 | const line = m[2]; // start from 1 894 | const column = m[3]; // start from 1 895 | return { url, line, column, code: null }; 896 | }); 897 | } 898 | 899 | function __dombasedxssfinder_vulns_push(sources, sinkLabel) { 900 | if (!document.body) { 901 | setTimeout(() => __dombasedxssfinder_vulns_push(sources, sinkLabel), 500); 902 | return; 903 | } 904 | let container = document.getElementById('#__dombasedxssfinder_result_container'); 905 | if (!container) { 906 | container = document.createElement('div'); 907 | container.style.display = 'none'; 908 | container.id = '__dombasedxssfinder_result_container'; 909 | document.body.appendChild(container); 910 | } 911 | if (container) { 912 | for (const source of sources) { 913 | const row = document.createElement('div'); 914 | row.style.display = 'none'; 915 | row.classList = '__dombasedxssfinder_result'; 916 | const result = { 917 | url: location.href, 918 | source, 919 | sink: __dombasedxssfinder_get_sink(sinkLabel) 920 | }; 921 | row.textContent = JSON.stringify(result); 922 | container.appendChild(row); 923 | console.debug('result', result); 924 | } 925 | } 926 | } 927 | 928 | function __dombasedxssfinder_get_source(label) { 929 | return { label, stacktrace: __dombasedxssfinder_get_stacktrace() }; 930 | } 931 | 932 | function __dombasedxssfinder_get_sink(label) { 933 | return { label, stacktrace: __dombasedxssfinder_get_stacktrace() }; 934 | } 935 | 936 | console.debug(`preload at ${location.href}`); 937 | } --------------------------------------------------------------------------------