├── netlify.toml ├── package.json ├── README.md ├── .gitignore ├── index.js └── index.html /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm run build" 3 | publish = "./" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dom-guard", 3 | "version": "0.0.3", 4 | "description": "Protect DOM nodes from tampering", 5 | "source": "index.js", 6 | "main": "dist/dom-guard.js", 7 | "exports": "./dist/dom-guard.modern.js", 8 | "module": "dist/dom-guard.module.js", 9 | "unpkg": "dist/dom-guard.umd.js", 10 | "files": [ 11 | "index.js", 12 | "README.md", 13 | "dist" 14 | ], 15 | "scripts": { 16 | "build": "microbundle --name DOMGuard", 17 | "dev": "microbundle watch", 18 | "serve": "serve .", 19 | "publish": "git push origin && git push origin --tags", 20 | "release:patch": "npm version patch && npm publish", 21 | "release:minor": "npm version minor && npm publish", 22 | "release:major": "npm version major && npm publish" 23 | }, 24 | "homepage": "https://github.com/DavidWells/dom-guard#readme", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/DavidWells/dom-guard" 28 | }, 29 | "keywords": [ 30 | "dom", 31 | "security" 32 | ], 33 | "author": "DavidWells", 34 | "license": "MIT", 35 | "devDependencies": { 36 | "microbundle": "^0.13.0", 37 | "serve": "^11.3.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DOM Guard 2 | 3 | Stop scammers from the manipulating DOM. [See demo](https://dom-guard.netlify.app) 4 | 5 | ## About 6 | 7 | Scammers are using dev tools to manipulate values in pages to trick unsuspecting victims into sending them money. These victims are typically the elderly. 😢 8 | 9 | They connect to their victim's machines via remote desktop software under the guise of tech support or some other well known company. 10 | 11 | The scammer then attempts to convince the victim they have received a larger than expected "refund" by manipulating the victim's bank user interface via chrome dev tools with the goal of getting the victim to mail them cash. 12 | 13 | See this video for [how the refund scams work](https://www.youtube.com/watch?v=J4mkZU2Y0as). 14 | 15 | DOMGuard is a small javascript library (~130 lines of code) & proof of concept to help put an end to these criminals. 16 | 17 | ## How does this work? 18 | 19 | Any changes attempted via Javascript are detected by MutationObserver. 20 | 21 | Additionally, guarded DOM nodes are checked via a "hearbeat" every `500ms` to ensure the values are what they should be. 22 | 23 | View the source code. 24 | 25 | ## Install 26 | 27 | ```bash 28 | npm install dom-guard 29 | ``` 30 | 31 | ## Usage 32 | 33 | ```js 34 | import DOMGuard from 'dom-guard' 35 | 36 | const guard = new DOMGuard({ 37 | selector: '#protected', // DOM Selector to protect 38 | heartbeat: 1000 // Check for manipulation every 1 second 39 | }) 40 | 41 | // Initialize DOMGuard on the #protected selector 42 | guard.init() 43 | 44 | // Turn off guard 45 | guard.disable() 46 | ``` 47 | 48 | ## Running the demo 49 | 50 | ```bash 51 | npm install 52 | npm run build 53 | npm run serve 54 | ``` 55 | 56 | ## Caveats 57 | 58 | Please note, there isn't a foolproof solution for stopping social engineering attacks against your users. 59 | 60 | Please educate your users on the dangers of these scams & add 2FA etc into your apps. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | dist 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | .env.test 62 | 63 | # parcel-bundler cache (https://parceljs.org/) 64 | .cache 65 | 66 | # next.js build output 67 | .next 68 | 69 | # nuxt.js build output 70 | .nuxt 71 | 72 | # vuepress build output 73 | .vuepress/dist 74 | 75 | # Serverless directories 76 | .serverless/ 77 | 78 | # FuseBox cache 79 | .fusebox/ 80 | 81 | # DynamoDB Local files 82 | .dynamodb/ 83 | 84 | # General 85 | .DS_Store 86 | .AppleDouble 87 | .LSOverride 88 | 89 | # Icon must end with two \r 90 | Icon 91 | 92 | 93 | # Thumbnails 94 | ._* 95 | 96 | # Files that might appear in the root of a volume 97 | .DocumentRevisions-V100 98 | .fseventsd 99 | .Spotlight-V100 100 | .TemporaryItems 101 | .Trashes 102 | .VolumeIcon.icns 103 | .com.apple.timemachine.donotpresent 104 | 105 | # Directories potentially created on remote AFP share 106 | .AppleDB 107 | .AppleDesktop 108 | Network Trash Folder 109 | Temporary Items 110 | .apdiske -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // inServer for SSR support 2 | const inServer = typeof window === 'undefined' 3 | 4 | class DOMGuard { 5 | constructor(opts = {}) { 6 | this.detachListener = () => {} 7 | if (inServer) { 8 | return 9 | } 10 | 11 | const originalElement = document.querySelector(opts.selector) 12 | if (!originalElement) { 13 | throw new Error(`Selector "${opts.selector}" not found`) 14 | } 15 | this.opts = opts 16 | this.selector = opts.selector 17 | this.initialText = normalizeText(originalElement.innerText) 18 | this.originalParent = originalElement.parentNode 19 | this.stashedClone = originalElement.cloneNode(true) 20 | } 21 | init() { 22 | /* Detect changes to any selector instantly */ 23 | const detachListener = domGuard({ 24 | selector: this.selector, 25 | initialText: this.initialText, 26 | stashedClone: this.stashedClone, 27 | originalParent: this.originalParent, 28 | }) 29 | /* Heartbeat to listen for changes via dev tools */ 30 | const heartbeat = setInterval(function () { 31 | restoreDom({ 32 | selector: this.selector, 33 | initialText: this.initialText, 34 | stashedClone: this.stashedClone, 35 | originalParent: this.originalParent, 36 | }) 37 | }.bind(this), this.opts.heartbeat || 500) 38 | 39 | this.detachListener = () => { 40 | clearInterval(heartbeat) 41 | detachListener() 42 | } 43 | /* Detach listener */ 44 | return this.detachListener 45 | } 46 | disable() { 47 | this.detachListener() 48 | } 49 | } 50 | 51 | function domGuard(apiOpts) { 52 | const { selector, initialText, originalParent, stashedClone, debug } = apiOpts 53 | /* attach observer to parent element */ 54 | let detachListener = observeDOM(originalParent, function(m) { 55 | var addedNodes = [], removedNodes = []; 56 | m.forEach(record => record.addedNodes.length & addedNodes.push(...record.addedNodes)) 57 | m.forEach(record => record.removedNodes.length & removedNodes.push(...record.removedNodes)) 58 | if (debug) { 59 | console.log('addedNodes:', addedNodes) 60 | console.log('removedNodes:', removedNodes) 61 | } 62 | if (addedNodes.length || removedNodes.length) { 63 | /* Disable listener to avoid infinite loop */ 64 | detachListener() 65 | /* Immediately restore DOM */ 66 | restoreDom({ selector, initialText, stashedClone, originalParent }) 67 | /* Reattach observeDOM Javascript listener */ 68 | detachListener = domGuard(apiOpts) 69 | } 70 | }) 71 | return detachListener 72 | } 73 | 74 | function normalizeText(str) { 75 | return str 76 | .split('\n') 77 | .map((x) => x.trim()) 78 | .filter((x) => Boolean(x)) 79 | .join('\n') 80 | } 81 | 82 | function restoreDom({ selector, initialText, originalParent, stashedClone, debug }) { 83 | if (inServer) return 84 | currentElement = document.querySelector(selector) 85 | if (!currentElement) return 86 | 87 | if (debug) { 88 | console.log(`────${selector}────`) 89 | console.log('currentElement', currentElement) 90 | console.log('stashedClone', stashedClone) 91 | } 92 | 93 | // const originalText = originalElement.innerText 94 | const currentElementText = normalizeText(currentElement.innerText) 95 | const stashedCloneText = normalizeText(stashedClone.innerText) 96 | if (debug) { 97 | console.log(`initialText: "${initialText}"`) 98 | console.log(`currentText: "${currentElementText}`) 99 | console.log(`persistedText: "${stashedCloneText}"`) 100 | } 101 | 102 | if (currentElementText !== initialText) { 103 | if (stashedCloneText === initialText) { 104 | if (debug) { 105 | console.log('DOM has changed! Reset it!') 106 | } 107 | const dirtyNode = stashedClone.cloneNode(true) 108 | /* Replace altered DOM with cloned original */ 109 | originalParent.replaceChild(dirtyNode, currentElement) 110 | } 111 | } 112 | } 113 | 114 | function observeDOM(element, callback) { 115 | if (inServer) return 116 | if (!element || element.nodeType !== 1) return 117 | 118 | var MutationObserver = window.MutationObserver || window.WebKitMutationObserver 119 | if (MutationObserver) { 120 | // define a new observer 121 | var mutationObserver = new MutationObserver(callback) 122 | // have the observer observe foo for changes in children 123 | mutationObserver.observe(element, { childList: true, subtree: true }) 124 | // Cleanup mutationObserver 125 | return () => { 126 | mutationObserver.disconnect() 127 | } 128 | } else if (window.addEventListener) { 129 | // browser support fallback 130 | element.addEventListener('DOMNodeInserted', callback, false) 131 | element.addEventListener('DOMNodeRemoved', callback, false) 132 | // Cleanup 133 | return () => { 134 | element.removeEventListener('DOMNodeInserted', callback) 135 | element.removeEventListener('DOMNodeRemoved', callback) 136 | } 137 | } 138 | } 139 | 140 | export default DOMGuard -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DOM Guard 7 | 8 | 9 | 10 | 81 | 82 | 83 | 86 | 87 |

DOMGuard - Stop scammers from the manipulating DOM

88 |
89 |
90 |
91 |

92 | Scammers are using dev tools to manipulate values in pages to trick unsuspecting victims into sending them money. These victims are typically the elderly. 😢 93 |

94 |

95 | They connect to their victim's machines via remote desktop software under the guise of tech support or some other well known company. 96 |

97 |

98 | The scammer then attempts to convince the victim they have received a larger than expected "refund" by manipulating the victim's bank user interface via chrome dev tools with the goal of getting the victim to mail them cash. 99 |

100 | 101 |

102 | DOMGuard is a small javascript library (~130 lines of code) & proof of concept to help put an end to these criminals. 103 |

104 | 105 |
106 | 107 |

See it in action

108 | 109 |
110 |

111 | Try to edit the green dollar amount below with Javascript or via chrome dev tools. 112 | The values are automatically reset if altered. 113 |

114 | $40,000 (protected, you cant change this)👈 115 |
116 | 117 |

Try to mutate the protected selected in the JS console

118 |
document.querySelector('#protected').innerText = 'ah ah ah didnt say the magic word'
119 | 120 |

No soup for you!

121 | 122 |
123 |

Here is a larger protected div. They are also guarded

124 |
$130,000 (protected, you cant change this)
125 |
$140,000 (protected, you cant change this) 126 | 127 |
128 |
$150,000 (protected, you cant change this)
129 |
130 | 131 |

Cool huh?

132 | 133 |
134 | 135 |
136 |

Here is a quick video on how the scammers operate.

137 | 138 |

Please share this with people you think might be vulnerable to such a scam ❤️

139 |

How does this work?

140 |

Any changes attempted via Javascript are detected by MutationObserver.

141 |

Additionally, guarded DOM nodes are checked via a "hearbeat" every `500ms` to ensure the values are what they should be.

142 |

View the source code.

143 |
144 | 145 |
146 | 147 | 169 | 170 | --------------------------------------------------------------------------------