├── 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 |
VIDEO
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 |
--------------------------------------------------------------------------------