├── .gitignore ├── images ├── risk_32.png ├── default_32.png ├── default_64.png ├── failure_32.png ├── validated_32.png ├── default_64@2x.png ├── temporary-extension-icon128.png ├── temporary-extension-icon32.png ├── temporary-extension-icon48.png ├── menu-badge.svg ├── chevron-right.svg ├── icon-badge.svg ├── error-badge.svg ├── x.svg ├── temporary-extension-icon.svg ├── warning-badge.svg ├── validated-badge.svg ├── circle-info.svg ├── circle-exclamation-mark.svg ├── circle-download-cta.svg ├── loading-header.svg ├── error-header.svg ├── validated-header.svg └── warning-header.svg ├── .prettierrc ├── .eslintrc.json ├── src ├── js │ ├── detectFBMeta.js │ ├── detectWAMeta.js │ ├── __tests__ │ │ ├── detectWAMeta-test.js │ │ ├── background-test.js │ │ └── contentUtils-test.js │ ├── popup.js │ ├── config.js │ ├── background.js │ └── contentUtils.js ├── html │ └── popup.html └── css │ └── popup.css ├── jest.setup.js ├── README.md ├── config ├── v3 │ └── manifest.json └── v2 │ └── manifest.json ├── LICENSE.md ├── CONTRIBUTING.md ├── rollup.config.js ├── package.json ├── CODE_OF_CONDUCT.md └── jest.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist/chrome 2 | dist/edge 3 | dist/firefox 4 | node_modules 5 | -------------------------------------------------------------------------------- /images/risk_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/meta-code-verify/main/images/risk_32.png -------------------------------------------------------------------------------- /images/default_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/meta-code-verify/main/images/default_32.png -------------------------------------------------------------------------------- /images/default_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/meta-code-verify/main/images/default_64.png -------------------------------------------------------------------------------- /images/failure_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/meta-code-verify/main/images/failure_32.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | arrowParens: "avoid", 3 | singleQuote: true, 4 | tabWidth: 2 5 | } 6 | -------------------------------------------------------------------------------- /images/validated_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/meta-code-verify/main/images/validated_32.png -------------------------------------------------------------------------------- /images/default_64@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/meta-code-verify/main/images/default_64@2x.png -------------------------------------------------------------------------------- /images/temporary-extension-icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/meta-code-verify/main/images/temporary-extension-icon128.png -------------------------------------------------------------------------------- /images/temporary-extension-icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/meta-code-verify/main/images/temporary-extension-icon32.png -------------------------------------------------------------------------------- /images/temporary-extension-icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apm/meta-code-verify/main/images/temporary-extension-icon48.png -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "jest": true, 6 | "webextensions": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/js/detectFBMeta.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { ORIGIN_TYPE } from './config.js'; 9 | import { startFor } from './contentUtils.js'; 10 | 11 | startFor(ORIGIN_TYPE.FACEBOOK); 12 | -------------------------------------------------------------------------------- /src/js/detectWAMeta.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { ORIGIN_TYPE } from './config.js'; 9 | import { startFor } from './contentUtils.js'; 10 | 11 | startFor(ORIGIN_TYPE.WHATSAPP); 12 | -------------------------------------------------------------------------------- /src/js/__tests__/detectWAMeta-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | describe('detectWAMeta', () => { 11 | it.todo('test extractMetaAndLoad'); 12 | it.todo('ensure extraMetaAndLoad is called statically'); 13 | it.todo('ensure extractMetaAndLoad is called after DOMContentReady'); 14 | }); 15 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | 3 | window.chrome = { 4 | browserAction: { 5 | setIcon: jest.fn(), 6 | setPopup: jest.fn(), 7 | }, 8 | runtime: { 9 | onMessage: { 10 | addListener: jest.fn(), 11 | }, 12 | sendMessage: jest.fn(), 13 | } 14 | }; 15 | 16 | window.crypto = { 17 | subtle: { 18 | digest: jest.fn(), 19 | } 20 | }; 21 | 22 | window.TextEncoder = function () {}; 23 | window.TextEncoder.encode = jest.fn(); 24 | 25 | window.Uint8Array = function () {}; 26 | -------------------------------------------------------------------------------- /images/menu-badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # meta-code-verify · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE.md) 2 | 3 | Code Verify is an extension for verifying the integrity of a web page. 4 | 5 | The idea is you can publish what JavaScript should appear on your site into a "manifest". The manifest consists of the hashes of all the JavaScript files in a given release. This browser extension can consume the manifest and verify that *only* that code executes, or else display a warning to the user. 6 | 7 | ## Installation 8 | 9 | You can install Code Verify from the extension store of Chrome, Firefox, or Edge. (Safari support coming soon, also direct links to the extensions.) 10 | 11 | ### [Code of Conduct](https://code.fb.com/codeofconduct) 12 | 13 | Meta has adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](https://code.fb.com/codeofconduct) so that you can understand what actions will and will not be tolerated. 14 | 15 | ### License 16 | 17 | React is [MIT licensed](./LICENSE). 18 | -------------------------------------------------------------------------------- /config/v3/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Code Verify", 4 | "version": "1.0.2", 5 | 6 | "description": "An extension to verify the code running in your browser matches what was published.", 7 | "action": { 8 | "default_title": "Code Verify", 9 | "default_icon": { 10 | "32": "default_32.png", 11 | "64": "default_64.png", 12 | "128": "default_64@2x.png" 13 | }, 14 | "default_popup": "loading.html" 15 | }, 16 | "icons": { 17 | "32": "default_32.png", 18 | "64": "default_64.png", 19 | "128": "default_64@2x.png" 20 | }, 21 | "background": { 22 | "service_worker": "background.js" 23 | }, 24 | "content_scripts": [ 25 | { 26 | "matches": ["*://*.whatsapp.com/*"], 27 | "exclude_matches": ["*://www.whatsapp.com/", "*://*.whatsapp.com/bt-manifest/*"], 28 | "js": ["contentWA.js"], 29 | "run_at": "document_start" 30 | } 31 | ], 32 | "host_permissions": [ 33 | "https://*.privacy-auditability.cloudflare.com/" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) Meta Platforms, Inc. and affiliates. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /config/v2/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Code Verify", 4 | "version": "1.0.2", 5 | 6 | "description": "An extension to verify the code running in your browser matches what was published.", 7 | "page_action": { 8 | "default_title": "Code Verify", 9 | "default_icon": { 10 | "32": "temporary-extension-icon32.png", 11 | "48": "temporary-extension-icon48.png", 12 | "128": "temporary-extension-icon128.png" 13 | }, 14 | "default_popup": "loading.html" 15 | }, 16 | "icons": { 17 | "32": "temporary-extension-icon32.png", 18 | "48": "temporary-extension-icon48.png", 19 | "128": "temporary-extension-icon128.png" 20 | }, 21 | "background": { 22 | "persistent": true, 23 | "scripts": ["background.js"] 24 | }, 25 | "content_scripts": [ 26 | { 27 | "matches": ["*://*.whatsapp.com/*"], 28 | "exclude_matches": ["*://www.whatsapp.com/", "*://*.whatsapp.com/bt-manifest/*"], 29 | "js": ["contentWA.js"], 30 | "run_at": "document_start" 31 | } 32 | ], 33 | "permissions": [ 34 | "https://*.privacy-auditability.cloudflare.com/", 35 | "https://web.whatsapp.com/" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to meta-code-verify 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Pull Requests 6 | We actively welcome your pull requests. 7 | 8 | 1. Fork the repo and create your branch from `master`. 9 | 2. If you've added code that should be tested, add tests. 10 | 3. If you've changed APIs, update the documentation. 11 | 4. Ensure the test suite passes. 12 | 5. Make sure your code lints. 13 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 14 | 15 | ## Contributor License Agreement ("CLA") 16 | In order to accept your pull request, we need you to submit a CLA. You only need 17 | to do this once to work on any of Meta's open source projects. 18 | 19 | Complete your CLA here: 20 | 21 | ## Issues 22 | We use GitHub issues to track public bugs. Please ensure your description is 23 | clear and has sufficient instructions to be able to reproduce the issue. 24 | 25 | Meta has a [bounty program](https://www.facebook.com/whitehat/) for the safe 26 | disclosure of security bugs. In those cases, please go through the process 27 | outlined on that page and do not file a public issue. 28 | 29 | ## License 30 | By contributing to meta-code-verify, you agree that your contributions will be licensed 31 | under the LICENSE file in the root directory of this source tree. 32 | -------------------------------------------------------------------------------- /images/chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /images/icon-badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /images/error-badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | input: 'src/js/detectWAMeta.js', 4 | output: [{ 5 | file: 'dist/chrome/contentWA.js', 6 | format: 'iife' 7 | }, { 8 | file: 'dist/edge/contentWA.js', 9 | format: 'iife' 10 | }, { 11 | file: 'dist/firefox/contentWA.js', 12 | format: 'iife' 13 | }] 14 | }, 15 | { 16 | input: 'src/js/detectFBMeta.js', 17 | output: [{ 18 | file: 'dist/chrome/contentFB.js', 19 | format: 'iife' 20 | }, { 21 | file: 'dist/edge/contentFB.js', 22 | format: 'iife' 23 | }, { 24 | file: 'dist/firefox/contentFB.js', 25 | format: 'iife' 26 | }] 27 | }, 28 | { 29 | input: 'src/js/background.js', 30 | output: [{ 31 | file: 'dist/chrome/background.js', 32 | format: 'iife' 33 | }, { 34 | file: 'dist/edge/background.js', 35 | format: 'iife' 36 | }, { 37 | file: 'dist/firefox/background.js', 38 | format: 'iife' 39 | }] 40 | }, 41 | { 42 | input: 'src/js/popup.js', 43 | output: [{ 44 | file: 'dist/chrome/popup.js', 45 | format: 'iife' 46 | }, { 47 | file: 'dist/edge/popup.js', 48 | format: 'iife' 49 | }, { 50 | file: 'dist/firefox/popup.js', 51 | format: 'iife' 52 | }] 53 | } 54 | 55 | ]; 56 | -------------------------------------------------------------------------------- /images/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /images/temporary-extension-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meta-code-verify", 3 | "version": "0.1.0", 4 | "description": "Browser extensions to verify code running in the browser against a published manifest", 5 | "main": "none", 6 | "repository": "git@github.com:facebookincubator/meta-code-verify.git", 7 | "author": "Richard Hansen ", 8 | "license": "MIT", 9 | "type": "module", 10 | "private": true, 11 | "scripts": { 12 | "build-local-dev": "yarn clean && yarn lint && yarn copyStaticFilesAndConfig && yarn makeBundle", 13 | "clean": "rm -rf dist/chrome/* dist/edge/* dist/firefox/*", 14 | "lint": "yarn makePrettier && yarn run eslint src/js/**", 15 | "copyChromeFilesAndConfig": "cp config/v3/* dist/chrome && cp images/* dist/chrome && cp src/html/* dist/chrome && cp src/css/* dist/chrome", 16 | "copyEdgeFilesAndConfig": "cp config/v3/* dist/edge && cp images/* dist/edge && cp src/html/* dist/edge && cp src/css/* dist/edge", 17 | "copyFirefoxFilesAndConfig": "cp config/v2/* dist/firefox && cp images/* dist/firefox && cp src/html/* dist/firefox && cp src/css/* dist/firefox", 18 | "copyStaticFilesAndConfig": "yarn copyChromeFilesAndConfig && yarn copyEdgeFilesAndConfig && yarn copyFirefoxFilesAndConfig", 19 | "makeBundle": "yarn run rollup -c", 20 | "makePrettier": "yarn run prettier --write \"src/**/*.js\"", 21 | "test": "yarn lint && node --experimental-vm-modules --no-warnings node_modules/jest/bin/jest.js" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^7.32.0", 25 | "jest": "^27.1.0", 26 | "prettier": "^2.3.2", 27 | "rollup": "^2.56.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /images/warning-badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /images/validated-badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /images/circle-info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /images/circle-exclamation-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /images/circle-download-cta.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/js/popup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | chrome.runtime.onMessage.addListener(message => { 9 | if (message && message.popup) { 10 | const state = message.popup.slice(message.popup.indexOf('=') + 1); 11 | updateDisplay(state); 12 | } 13 | }); 14 | 15 | function attachListeners() { 16 | const menuButtonList = document.getElementsByClassName('menu'); 17 | Array.from(menuButtonList).forEach(menuButton => { 18 | menuButton.addEventListener('click', () => updateDisplay('menu')); 19 | }); 20 | 21 | const closeMenuButton = document.getElementById('close_menu'); 22 | closeMenuButton.addEventListener('click', () => window.close()); 23 | 24 | const menuRowList = document.getElementsByClassName('menu_row'); 25 | menuRowList[0].addEventListener('click', () => { 26 | chrome.tabs.create({ 27 | url: 'https://faq.whatsapp.com/web/security-and-privacy/about-code-verify', 28 | }); 29 | }); 30 | menuRowList[0].style.cursor = 'pointer'; 31 | menuRowList[1].addEventListener('click', () => updateDisplay('download')); 32 | menuRowList[1].style.cursor = 'pointer'; 33 | 34 | const downloadTextList = document.getElementsByClassName( 35 | 'status_message_highlight' 36 | ); 37 | downloadTextList[0].addEventListener('click', () => 38 | updateDisplay('download') 39 | ); 40 | downloadTextList[0].style.cursor = 'pointer'; 41 | 42 | const learnMoreList = document.getElementsByClassName( 43 | 'anomaly_learn_more_button' 44 | ); 45 | learnMoreList[0].addEventListener('click', () => { 46 | chrome.tabs.create({ 47 | url: 'https://faq.whatsapp.com/web/security-and-privacy/why-am-i-seeing-a-validation-failure-warning', 48 | }); 49 | }); 50 | learnMoreList[0].style.cursor = 'pointer'; 51 | 52 | const riskLearnMoreList = document.getElementsByClassName( 53 | 'risk_learn_more_button' 54 | ); 55 | riskLearnMoreList[0].addEventListener('click', () => { 56 | chrome.tabs.create({ 57 | url: 'https://faq.whatsapp.com/web/security-and-privacy/why-am-i-seeing-a-possible-risk-detected-warning', 58 | }); 59 | }); 60 | riskLearnMoreList[0].style.cursor = 'pointer'; 61 | 62 | const retryButtonList = document.getElementsByClassName('retry_button'); 63 | Array.from(retryButtonList).forEach(retryButton => { 64 | retryButton.addEventListener('click', () => { 65 | chrome.tabs.reload(); 66 | }); 67 | retryButton.style.cursor = 'pointer'; 68 | }); 69 | 70 | const timeoutLearnMoreList = document.getElementsByClassName( 71 | 'timeout_learn_more_button' 72 | ); 73 | timeoutLearnMoreList[0].addEventListener('click', () => { 74 | chrome.tabs.create({ 75 | url: 'https://faq.whatsapp.com/web/security-and-privacy/why-am-i-seeing-a-network-timeout-error', 76 | }); 77 | }); 78 | timeoutLearnMoreList[0].style.cursor = 'pointer'; 79 | } 80 | 81 | function updateDisplay(state) { 82 | Array.from(document.getElementsByClassName('state_boundary')).forEach( 83 | element => { 84 | if (element.id == state) { 85 | element.style.display = 'flex'; 86 | document.body.className = state + '_body'; 87 | } else { 88 | element.style.display = 'none'; 89 | } 90 | } 91 | ); 92 | } 93 | 94 | function loadUp() { 95 | const params = new URL(document.location).searchParams; 96 | const state = params.get('state'); 97 | updateDisplay(state); 98 | attachListeners(); 99 | } 100 | 101 | loadUp(); 102 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when there is a 56 | reasonable belief that an individual's behavior may have a negative impact on 57 | the project or its community. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported by contacting the project team at . All 63 | complaints will be reviewed and investigated and will result in a response that 64 | is deemed necessary and appropriate to the circumstances. The project team is 65 | obligated to maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted separately. 67 | 68 | Project maintainers who do not follow or enforce the Code of Conduct in good 69 | faith may face temporary or permanent repercussions as determined by other 70 | members of the project's leadership. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 75 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | 79 | For answers to common questions about this code of conduct, see 80 | https://www.contributor-covenant.org/faq 81 | -------------------------------------------------------------------------------- /src/js/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | export const ICON_STATE = { 9 | DEFAULT: { badge: 'icon-badge.svg', popup: 'popup.html?state=loading' }, 10 | INVALID_HARD: { 11 | // badge: 'error-badge.svg', 12 | badge: { 13 | 32: 'failure_32.png', 14 | }, 15 | popup: 'popup.html?state=error', 16 | }, 17 | INVALID_SOFT: { 18 | // badge: 'error-badge.svg', 19 | badge: { 20 | 32: 'failure_32.png', 21 | }, 22 | popup: 'popup.html?state=error', 23 | }, 24 | PROCESSING: { 25 | // badge: 'icon-badge.svg', 26 | badge: { 27 | 32: 'default_32.png', 28 | }, 29 | popup: 'popup.html?state=loading', 30 | }, 31 | VALID: { 32 | // badge: 'validated-badge.svg', 33 | badge: { 34 | 32: 'validated_32.png', 35 | }, 36 | popup: 'popup.html?state=valid', 37 | }, 38 | WARNING_RISK: { 39 | // badge: 'warning-badge.svg', 40 | badge: { 41 | 32: 'risk_32.png', 42 | }, 43 | popup: 'popup.html?state=warning_risk', 44 | }, 45 | WARNING_TIMEOUT: { 46 | // badge: 'warning-badge.svg', 47 | badge: { 48 | 32: 'risk_32.png', 49 | }, 50 | popup: 'popup.html?state=warning_timeout', 51 | }, 52 | }; 53 | 54 | export const KNOWN_EXTENSION_HASHES = [ 55 | '', // Chrome - Dynamic: StopAll Ads 56 | '727bfede71f473991faeb7f4b65632c93e7f7d17189f1b3d952cd990cd150808', // Chrome and Edge: Avast Online Security & Privacy v21.0.101 57 | 'c09a2e7b2fa97705c9afe890498e1f620ede4bd2968cfef7421080a8f9f0d8f9', // Chrome: Privacy Badger v2021.11.23.1 58 | '04c354b90b330f4cac2678ccd311e5d2a6e8b57815510b176ddbed8d52595726', // Chrome: LastPass v4.88.0 59 | 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', // Chrome: AdLock - adblocker & privacy protection v0.1.30 60 | '', // Chrome - Dynamic: AdBlocker Ultimate v3.7.15 61 | '', // Chrome - Dynamic: DuckDuckGo Privacy Essentials v2022.2.22 62 | '', // Chrome - Dynamic: Crystal Ad block v1.3.9 63 | '', // Chrome - Dynamic: AdBlock — best ad blocker v4.43.0 64 | '4ae6b4dcefb37952cef704c39fe3e8d675bd32c54302984e747ba6768541862a', // Chrome: Vue.js devtools v6.0.12 65 | '91fecf0ca4c2260f8a18d1c371d717e656e98a0015f0206379afe662746d6009', // Chrome: Vue.js devtools v6.0.12 66 | 'e64b3a9472f559611158a628da06e770ce8cc3d0f8395849072a0199bae705f9', // FF: Total Adblock-Ad Blocker v2.10.0 *and* FF/Edge BitGuard v1.0 67 | 'c924b9ed122066e5420b125a1accb787c3856c4a422fe9bde47d1f40660271a6', // FF: Smart Blocker v1.0.2 68 | '', // FF: Popup Blocker(strict) 69 | '', // FF - Dynamic: Privacy Tweaks 70 | '', // FF: Privacy Possum 71 | '', // FF - Dynamic: Adblocker X v2.0.5 72 | '', // FF - Dynamic: AdBlocker Ultimate v3.7.15 73 | '', // FF - Dynamic: Cloudopt AdBlocker v2.3.0 74 | '', // Edge - Dynamic: Epsilon Ad blocker v1.4.6 75 | '7a69d1fb29471a9962307f7882adade784141d02617e233eb366ae5f63fd9dd8', // Edge and FF: Minimal Consent v1.0.9 76 | 'd768396bbfda57a3defb0aeba5d9b9aefef562d8204520668f9e275c68455a0c', // Edge: Writer from Writer.com v1.63.2 77 | '', // Edge - Dynamic: AdBlock --- best ad blocker v4.43.0 78 | '855e2fd1368fc12a14159e26ed3132e6567e8443f8b75081265b93845b865103', // Edge and FF: AdGuard AdBlocker v3.6.17 79 | 'deda33bced5f2014562e03f8c82a2a16df074a2bc6be6eceba78274056e41372', // Edge: Netcraft Extension v1.16.8 80 | '', // Edge - Dynamic: Hola ad remover v1.194.444 81 | '', // Edge - Dynamic: Tau adblock v1.4.1 82 | ]; 83 | 84 | export const MESSAGE_TYPE = { 85 | DEBUG: 'DEBUG', 86 | GET_DEBUG: 'GET_DEBUG', 87 | JS_WITH_SRC: 'JS_WITH_SRC', 88 | LOAD_MANIFEST: 'LOAD_MANIFEST', 89 | POPUP_STATE: 'POPUP_STATE', 90 | RAW_JS: 'RAW_JS', 91 | UPDATE_ICON: 'UPDATE_ICON', 92 | }; 93 | 94 | export const ORIGIN_HOST = { 95 | FACEBOOK: 'facebook.com', 96 | WHATSAPP: 'whatsapp.com', 97 | }; 98 | 99 | export const ORIGIN_TIMEOUT = { 100 | FACEBOOK: 176400000, 101 | WHATSAPP: 0, 102 | }; 103 | 104 | export const ORIGIN_TYPE = { 105 | FACEBOOK: 'FACEBOOK', 106 | WHATSAPP: 'WHATSAPP', 107 | }; 108 | -------------------------------------------------------------------------------- /src/html/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 |
Validated
15 |
Web page code verified.
16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 |
Checking...
26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 |
Possible Risk Detected
36 |
Cannot validate the page due to another browser extension. Consider pausing the other extension(s) and re-trying.
37 |
38 | 39 | 40 |
41 |
42 |
43 |
44 |
45 | 46 | 47 |
48 |
49 | 50 |
Network Timed Out
51 |
Unable to validate this page.
52 |
53 | 54 | 55 |
56 |
57 |
58 |
59 |
60 | 61 | 62 |
63 |
64 | 65 |
Validation Failure
66 |
The source code on this page doesn't match what was sent to other users. Download the source code to examine why the page was not validated.
67 |
68 | 69 |
70 |
71 |
72 | 95 |
96 |
97 | 98 | 99 |
100 |
101 |
Report a Bug
102 |
Having a problem? Click below to report the bug.
103 |
104 | 105 |
106 |
107 |
108 |
109 |
110 | 111 | 112 |

Download

113 |
114 | 115 |
116 |
117 |
Download a .zip file containing all running JavaScript on the page.
118 |
119 | 120 |
121 |
122 |
123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/css/popup.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | .action_bar { 9 | flex-direction: row; 10 | justify-content: space-between; 11 | margin: 24px 13px 0 13px; 12 | } 13 | 14 | .badge { 15 | height: 20px; 16 | } 17 | 18 | .content_body { 19 | align-items: center; 20 | display: flex; 21 | justify-content: center; 22 | flex-direction: column; 23 | } 24 | 25 | .anomaly_learn_more_button, .report_issue_button { 26 | border: 1px solid #CBD2D9; 27 | box-sizing: border-box; 28 | border-radius: 4px; 29 | height: 36px; 30 | width: 230px; 31 | background: #1C2B33; 32 | font-family: SF Pro Text; 33 | font-style: normal; 34 | font-weight: 500; 35 | font-size: 15px; 36 | line-height: 20px; 37 | text-align: center; 38 | letter-spacing: -0.23px; 39 | color: #FFFFFF; 40 | } 41 | 42 | .download_button, 43 | .risk_learn_more_button, 44 | .timeout_learn_more_button { 45 | border: 1px solid #CBD2D9; 46 | box-sizing: border-box; 47 | border-radius: 4px; 48 | height: 36px; 49 | width: 111px; 50 | background: #FFFFFF; 51 | font-family: SF Pro Text; 52 | font-style: normal; 53 | font-weight: 500; 54 | font-size: 15px; 55 | line-height: 20px; 56 | text-align: center; 57 | letter-spacing: -0.23px; 58 | color: #1C2B33; 59 | } 60 | 61 | .download_header { 62 | display: flex; 63 | } 64 | 65 | .download_title { 66 | font-family: Optimistic Display; 67 | font-style: normal; 68 | font-weight: bold; 69 | font-size: 17px; 70 | line-height: 22px; 71 | letter-spacing: 0.24px; 72 | color: #000000; 73 | height: 22px; 74 | width: 84px; 75 | margin: 0px 0px 0px 12px; 76 | padding-bottom: 21px; 77 | } 78 | 79 | .error_body { 80 | width: 262px; 81 | height: 288px; 82 | background: #FFFFFF; 83 | margin: 0; 84 | } 85 | 86 | header { 87 | display: flex; 88 | justify-content: space-between; 89 | padding: 16px 16px 0 16px; 90 | } 91 | 92 | .menu { 93 | cursor: pointer; 94 | height: 20px; 95 | } 96 | 97 | .menu_body { 98 | height: 227px; 99 | width: 256px; 100 | margin: 0px; 101 | } 102 | 103 | .menu_right_sidebar { 104 | display: flex; 105 | flex-direction: column; 106 | background: #F1F4F7; 107 | box-shadow: 0px 0px 16px rgba(52, 72, 84, 0.05); 108 | height: 227px; 109 | width: 209px; 110 | } 111 | 112 | .menu_row { 113 | display: flex; 114 | flex-direction: row; 115 | align-items: center; 116 | padding: 8px 16px; 117 | } 118 | 119 | .menu_row > p { 120 | font-family: SF Pro Text; 121 | font-style: normal; 122 | font-weight: 500; 123 | font-size: 15px; 124 | line-height: 20px; 125 | letter-spacing: -0.23px; 126 | color: #1C2B33; 127 | flex: none; 128 | order: 1; 129 | flex-grow: 1; 130 | margin: 0px 12px; 131 | width: 113px; 132 | } 133 | 134 | .menu_title { 135 | display: flex; 136 | flex-direction: row; 137 | justify-content: space-between; 138 | font-family: Optimistic Display; 139 | font-style: normal; 140 | font-weight: bold; 141 | font-size: 17px; 142 | line-height: 22px; 143 | letter-spacing: 0.24px; 144 | color: #000000; 145 | flex: none; 146 | order: 0; 147 | flex-grow: 0; 148 | margin: 0px 16px; 149 | } 150 | 151 | .menu_title > p { 152 | width: 138px; 153 | } 154 | 155 | .menu_title > button { 156 | border: none; 157 | cursor: pointer; 158 | width: 24px; 159 | height: 24px; 160 | margin: 16px 0 0; 161 | } 162 | 163 | .menu_top_level { 164 | display: flex; 165 | flex-direction: row; 166 | } 167 | 168 | .menu_top_level > .badge { 169 | margin: 13px 12px 0 15px; 170 | } 171 | 172 | .loading_body { 173 | width: 257px; 174 | height: 154px; 175 | background: #FFFFFF; 176 | margin: 0; 177 | } 178 | 179 | .loading_body_image { 180 | height: 32px; 181 | margin-top: 24px; 182 | width: 158px; 183 | } 184 | 185 | .retry_button { 186 | border: 1px solid #CBD2D9; 187 | box-sizing: border-box; 188 | border-radius: 4px; 189 | height: 36px; 190 | width: 111px; 191 | background: #1C2B33; 192 | font-family: SF Pro Text; 193 | font-style: normal; 194 | font-weight: 500; 195 | font-size: 15px; 196 | line-height: 20px; 197 | text-align: center; 198 | letter-spacing: -0.23px; 199 | color: #FFFFFF; 200 | } 201 | 202 | .row_image { 203 | flex: none; 204 | order: 0; 205 | flex-grow: 0; 206 | } 207 | 208 | .row_nav { 209 | flex: none; 210 | order: 2; 211 | flex-grow: 0; 212 | margin: 0px 12px; 213 | } 214 | 215 | .state_boundary { 216 | flex-direction: column; 217 | } 218 | 219 | .status_header { 220 | font-family: SF Pro Text; 221 | font-style: normal; 222 | font-weight: 600; 223 | font-size: 13px; 224 | line-height: 18px; 225 | 226 | text-align: center; 227 | letter-spacing: -0.08px; 228 | 229 | color: #1C2B33; 230 | margin-top: 12px; 231 | } 232 | 233 | .status_message { 234 | font-family: SF Pro Text; 235 | font-style: normal; 236 | font-weight: normal; 237 | font-size: 13px; 238 | line-height: 18px; 239 | text-align: center; 240 | letter-spacing: -0.08px; 241 | color: #63788A; 242 | margin: 0 36px; 243 | } 244 | 245 | .status_message_highlight { 246 | color: #1C2B33; 247 | } 248 | 249 | .valid_body { 250 | width: 262px; 251 | height: 164px; 252 | background: #FFFFFF; 253 | margin: 0; 254 | } 255 | 256 | .validated_body_image { 257 | height: 32px; 258 | margin-top: 24px; 259 | width: 158px; 260 | } 261 | 262 | .warning_risk_body { 263 | width: 262px; 264 | height: 270px; 265 | background: #FFFFFF; 266 | margin: 0; 267 | } 268 | 269 | .warning_timeout_body { 270 | width: 262px; 271 | height: 216px; 272 | background: #FFFFFF; 273 | margin: 0; 274 | } 275 | 276 | .warning_body_image { 277 | height: 32px; 278 | margin-top: 24px; 279 | width: 158px; 280 | } 281 | -------------------------------------------------------------------------------- /images/loading-header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /images/error-header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /images/validated-header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /images/warning-header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | export default { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/tmp/rrh/jest_2fus", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | clearMocks: true, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | // coverageDirectory: undefined, 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: "v8", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "jsx", 77 | // "ts", 78 | // "tsx", 79 | // "json", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | // preset: undefined, 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state between every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state between every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | // setupFiles: [], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | setupFilesAfterEnv: ["./jest.setup.js"], 132 | 133 | // The number of seconds after which a test is considered as slow and reported as such in the results. 134 | // slowTestThreshold: 5, 135 | 136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 137 | // snapshotSerializers: [], 138 | 139 | // The test environment that will be used for testing 140 | testEnvironment: "jsdom", 141 | 142 | // Options that will be passed to the testEnvironment 143 | // testEnvironmentOptions: {}, 144 | 145 | // Adds a location field to test results 146 | // testLocationInResults: false, 147 | 148 | // The glob patterns Jest uses to detect test files 149 | // testMatch: [ 150 | // "**/__tests__/**/*.[jt]s?(x)", 151 | // "**/?(*.)+(spec|test).[tj]s?(x)" 152 | // ], 153 | 154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 155 | // testPathIgnorePatterns: [ 156 | // "/node_modules/" 157 | // ], 158 | 159 | // The regexp pattern or array of patterns that Jest uses to detect test files 160 | // testRegex: [], 161 | 162 | // This option allows the use of a custom results processor 163 | // testResultsProcessor: undefined, 164 | 165 | // This option allows use of a custom test runner 166 | // testRunner: "jest-circus/runner", 167 | 168 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 169 | // testURL: "http://localhost", 170 | 171 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 172 | timers: "real", 173 | 174 | // A map from regular expressions to paths to transformers 175 | transform: {}, 176 | 177 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 178 | // transformIgnorePatterns: [ 179 | // "/node_modules/", 180 | // "\\.pnp\\.[^\\/]+$" 181 | // ], 182 | 183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 184 | // unmockedModulePathPatterns: undefined, 185 | 186 | // Indicates whether each individual test should be reported during the run 187 | // verbose: undefined, 188 | 189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 190 | // watchPathIgnorePatterns: [], 191 | 192 | // Whether to use watchman for file crawling 193 | // watchman: true, 194 | }; 195 | -------------------------------------------------------------------------------- /src/js/__tests__/background-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | import { jest } from '@jest/globals'; 11 | import { MESSAGE_TYPE, ORIGIN_TYPE } from '../config.js'; 12 | import { handleMessages } from '../background.js'; 13 | 14 | describe('background', () => { 15 | beforeEach(() => { 16 | window.chrome.browserAction.setIcon = jest.fn(() => {}); 17 | }); 18 | describe('UPDATE_ICON', () => { 19 | it('should send update icon message when receiving icon update', () => { 20 | const testIcon = 'testIcon'; 21 | handleMessages( 22 | { 23 | icon: testIcon, 24 | type: MESSAGE_TYPE.UPDATE_ICON, 25 | }, 26 | null, 27 | () => {} 28 | ); 29 | expect(window.chrome.browserAction.setIcon.mock.calls.length).toBe(1); 30 | }); 31 | }); 32 | 33 | describe('LOAD_MANIFEST', () => { 34 | it('should load manifest when origin is missing', async () => { 35 | window.fetch = jest.fn(); 36 | window.fetch.mockReturnValueOnce( 37 | Promise.resolve({ 38 | json: () => Promise.resolve({ 1: { '/somepath': 'somehash' } }), 39 | }) 40 | ); 41 | const mockSendResponse = jest.fn(); 42 | const handleMessagesReturnValue = handleMessages( 43 | { 44 | origin: ORIGIN_TYPE.WHATSAPP, 45 | type: MESSAGE_TYPE.LOAD_MANIFEST, 46 | version: '1', 47 | }, 48 | null, 49 | mockSendResponse 50 | ); 51 | await (() => new Promise(res => setTimeout(res, 10)))(); 52 | expect(window.fetch.mock.calls.length).toBe(1); 53 | expect(handleMessagesReturnValue).toBe(true); 54 | expect(mockSendResponse.mock.calls.length).toBe(1); 55 | expect(mockSendResponse.mock.calls[0][0].valid).toBe(true); 56 | }); 57 | it('should load manifest when manifest is missing', async () => { 58 | window.fetch = jest.fn(); 59 | window.fetch.mockReturnValueOnce( 60 | Promise.resolve({ 61 | json: () => 62 | Promise.resolve({ 2: { '/someotherpath': 'someotherhash' } }), 63 | }) 64 | ); 65 | const mockSendResponse = jest.fn(); 66 | const handleMessagesReturnValue = handleMessages( 67 | { 68 | origin: ORIGIN_TYPE.WHATSAPP, 69 | type: MESSAGE_TYPE.LOAD_MANIFEST, 70 | version: '2', 71 | }, 72 | null, 73 | mockSendResponse 74 | ); 75 | await (() => new Promise(res => setTimeout(res, 10)))(); 76 | expect(window.fetch.mock.calls.length).toBe(1); 77 | expect(handleMessagesReturnValue).toBe(true); 78 | expect(mockSendResponse.mock.calls.length).toBe(1); 79 | expect(mockSendResponse.mock.calls[0][0].valid).toBe(true); 80 | }); 81 | it('return valid when manifest and origin are found in cache', async () => { 82 | window.fetch = jest.fn(); 83 | const mockSendResponse = jest.fn(); 84 | const handleMessagesReturnValue = handleMessages( 85 | { 86 | origin: ORIGIN_TYPE.WHATSAPP, 87 | type: MESSAGE_TYPE.LOAD_MANIFEST, 88 | version: '1', 89 | }, 90 | null, 91 | mockSendResponse 92 | ); 93 | await (() => new Promise(res => setTimeout(res, 10)))(); 94 | expect(window.fetch.mock.calls.length).toBe(0); 95 | expect(handleMessagesReturnValue).toBe(undefined); 96 | expect(mockSendResponse.mock.calls.length).toBe(1); 97 | expect(mockSendResponse.mock.calls[0][0].valid).toBe(true); 98 | }); 99 | }); 100 | 101 | describe('JS_WITH_SRC', () => { 102 | it('should return false when no matching origin', () => { 103 | const mockSendResponse = jest.fn(); 104 | handleMessages( 105 | { 106 | origin: 'NOT_AN_ORIGIN', 107 | type: MESSAGE_TYPE.JS_WITH_SRC, 108 | version: '1', 109 | }, 110 | null, 111 | mockSendResponse 112 | ); 113 | expect(mockSendResponse.mock.calls.length).toBe(1); 114 | expect(mockSendResponse.mock.calls[0][0].valid).toBe(false); 115 | expect(mockSendResponse.mock.calls[0][0].reason).toBe( 116 | 'no matching origin' 117 | ); 118 | }); 119 | it('should return false when no matching manifest', () => { 120 | const mockSendResponse = jest.fn(); 121 | handleMessages( 122 | { 123 | origin: ORIGIN_TYPE.WHATSAPP, 124 | type: MESSAGE_TYPE.JS_WITH_SRC, 125 | version: 'NOT_A_VALID_VERSION', 126 | }, 127 | null, 128 | mockSendResponse 129 | ); 130 | expect(mockSendResponse.mock.calls.length).toBe(1); 131 | expect(mockSendResponse.mock.calls[0][0].valid).toBe(false); 132 | expect(mockSendResponse.mock.calls[0][0].reason).toBe( 133 | 'no matching manifest' 134 | ); 135 | }); 136 | it('should return false when no matching hash', () => { 137 | const mockSendResponse = jest.fn(); 138 | handleMessages( 139 | { 140 | origin: ORIGIN_TYPE.WHATSAPP, 141 | type: MESSAGE_TYPE.JS_WITH_SRC, 142 | src: 'https://www.notavalidurl.com/nottherightpath', 143 | version: '1', 144 | }, 145 | null, 146 | mockSendResponse 147 | ); 148 | expect(mockSendResponse.mock.calls.length).toBe(1); 149 | expect(mockSendResponse.mock.calls[0][0].valid).toBe(false); 150 | expect(mockSendResponse.mock.calls[0][0].reason).toBe('no matching hash'); 151 | }); 152 | it('should return false if the hashes do not match', async () => { 153 | window.fetch = jest.fn(); 154 | window.fetch.mockReturnValueOnce( 155 | Promise.resolve({ 156 | text: () => 157 | Promise.resolve('console.log("all the JavaScript goes here")'), 158 | }) 159 | ); 160 | const encodeMock = jest.fn(); 161 | window.TextEncoder = function () { 162 | return { 163 | encode: encodeMock, 164 | }; 165 | }; 166 | encodeMock.mockReturnValueOnce('abc'); 167 | window.crypto.subtle.digest = jest 168 | .fn() 169 | .mockReturnValueOnce(Promise.resolve('def')); 170 | window.Uint8Array = jest.fn().mockReturnValueOnce(['somefakehash']); 171 | const mockSendResponse = jest.fn(); 172 | handleMessages( 173 | { 174 | origin: ORIGIN_TYPE.WHATSAPP, 175 | type: MESSAGE_TYPE.JS_WITH_SRC, 176 | src: 'https://www.notavalidurl.com/someotherpath', 177 | version: '2', 178 | }, 179 | null, 180 | mockSendResponse 181 | ); 182 | await (() => new Promise(res => setTimeout(res, 10)))(); 183 | expect(mockSendResponse.mock.calls.length).toBe(1); 184 | expect(mockSendResponse.mock.calls[0][0].valid).toBe(false); 185 | }); 186 | it('should return true iff the hashes match', async () => { 187 | window.fetch = jest.fn(); 188 | window.fetch.mockReturnValueOnce( 189 | Promise.resolve({ 190 | text: () => 191 | Promise.resolve('console.log("all the JavaScript goes here")'), 192 | }) 193 | ); 194 | const encodeMock = jest.fn(); 195 | window.TextEncoder = function () { 196 | return { 197 | encode: encodeMock, 198 | }; 199 | }; 200 | encodeMock.mockReturnValueOnce('abc'); 201 | window.crypto.subtle.digest = jest 202 | .fn() 203 | .mockReturnValueOnce(Promise.resolve('def')); 204 | window.Uint8Array = jest.fn().mockReturnValueOnce(['someotherhash']); 205 | const mockSendResponse = jest.fn(); 206 | handleMessages( 207 | { 208 | origin: ORIGIN_TYPE.WHATSAPP, 209 | type: MESSAGE_TYPE.JS_WITH_SRC, 210 | src: 'https://www.notavalidurl.com/someotherpath', 211 | version: '2', 212 | }, 213 | null, 214 | mockSendResponse 215 | ); 216 | await (() => new Promise(res => setTimeout(res, 10)))(); 217 | expect(mockSendResponse.mock.calls.length).toBe(1); 218 | expect(mockSendResponse.mock.calls[0][0].valid).toBe(true); 219 | }); 220 | }); 221 | 222 | describe('RAW_JS', () => { 223 | it('should return false when no matching origin', () => { 224 | const mockSendResponse = jest.fn(); 225 | handleMessages( 226 | { 227 | origin: 'NOT_AN_ORIGIN', 228 | type: MESSAGE_TYPE.RAW_JS, 229 | version: '1', 230 | }, 231 | null, 232 | mockSendResponse 233 | ); 234 | expect(mockSendResponse.mock.calls.length).toBe(1); 235 | expect(mockSendResponse.mock.calls[0][0].valid).toBe(false); 236 | expect(mockSendResponse.mock.calls[0][0].reason).toBe( 237 | 'no matching origin' 238 | ); 239 | }); 240 | it('should return false when no matching manifest', () => { 241 | const mockSendResponse = jest.fn(); 242 | handleMessages( 243 | { 244 | origin: ORIGIN_TYPE.WHATSAPP, 245 | type: MESSAGE_TYPE.RAW_JS, 246 | version: 'NOT_A_VALID_VERSION', 247 | }, 248 | null, 249 | mockSendResponse 250 | ); 251 | expect(mockSendResponse.mock.calls.length).toBe(1); 252 | expect(mockSendResponse.mock.calls[0][0].valid).toBe(false); 253 | expect(mockSendResponse.mock.calls[0][0].reason).toBe( 254 | 'no matching manifest' 255 | ); 256 | }); 257 | it('should return false when no matching hash', async () => { 258 | const mockSendResponse = jest.fn(); 259 | const encodeMock = jest.fn(); 260 | window.TextEncoder = function () { 261 | return { 262 | encode: encodeMock, 263 | }; 264 | }; 265 | encodeMock.mockReturnValueOnce('abc'); 266 | window.crypto.subtle.digest = jest 267 | .fn() 268 | .mockReturnValueOnce(Promise.resolve('def')); 269 | window.Uint8Array = jest.fn().mockReturnValueOnce(['somefakehash']); 270 | handleMessages( 271 | { 272 | origin: ORIGIN_TYPE.WHATSAPP, 273 | type: MESSAGE_TYPE.RAW_JS, 274 | src: 'https://www.notavalidurl.com/nottherightpath', 275 | version: '1', 276 | }, 277 | null, 278 | mockSendResponse 279 | ); 280 | await (() => new Promise(res => setTimeout(res, 10)))(); 281 | expect(mockSendResponse.mock.calls.length).toBe(1); 282 | expect(mockSendResponse.mock.calls[0][0].valid).toBe(false); 283 | expect(mockSendResponse.mock.calls[0][0].reason).toBe('no matching hash'); 284 | }); 285 | it('should return false if the hashes do not match', async () => { 286 | const encodeMock = jest.fn(); 287 | window.TextEncoder = function () { 288 | return { 289 | encode: encodeMock, 290 | }; 291 | }; 292 | encodeMock.mockReturnValueOnce('abc'); 293 | window.crypto.subtle.digest = jest 294 | .fn() 295 | .mockReturnValueOnce(Promise.resolve('def')); 296 | window.Uint8Array = jest.fn().mockReturnValueOnce(['somefakehash']); 297 | const mockSendResponse = jest.fn(); 298 | handleMessages( 299 | { 300 | origin: ORIGIN_TYPE.WHATSAPP, 301 | type: MESSAGE_TYPE.RAW_JS, 302 | rawjs: 'console.log("all the JavaScript goes here");', 303 | version: '2', 304 | }, 305 | null, 306 | mockSendResponse 307 | ); 308 | await (() => new Promise(res => setTimeout(res, 10)))(); 309 | expect(mockSendResponse.mock.calls.length).toBe(1); 310 | expect(mockSendResponse.mock.calls[0][0].valid).toBe(false); 311 | }); 312 | it('should return true iff the hashes match', async () => { 313 | const encodeMock = jest.fn(); 314 | window.TextEncoder = function () { 315 | return { 316 | encode: encodeMock, 317 | }; 318 | }; 319 | encodeMock.mockReturnValueOnce('abc'); 320 | window.crypto.subtle.digest = jest 321 | .fn() 322 | .mockReturnValueOnce(Promise.resolve('def')); 323 | window.Uint8Array = jest.fn().mockReturnValueOnce(['someotherhash']); 324 | const mockSendResponse = jest.fn(); 325 | handleMessages( 326 | { 327 | origin: ORIGIN_TYPE.WHATSAPP, 328 | lookupKey: '/someotherpath', 329 | type: MESSAGE_TYPE.RAW_JS, 330 | rawjs: 'console.log("all the JavaScript goes here");', 331 | version: '2', 332 | }, 333 | null, 334 | mockSendResponse 335 | ); 336 | await (() => new Promise(res => setTimeout(res, 10)))(); 337 | expect(mockSendResponse.mock.calls.length).toBe(1); 338 | expect(mockSendResponse.mock.calls[0][0].valid).toBe(true); 339 | }); 340 | }); 341 | }); 342 | -------------------------------------------------------------------------------- /src/js/__tests__/contentUtils-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | import { jest } from '@jest/globals'; 11 | import { ICON_STATE, MESSAGE_TYPE, ORIGIN_TYPE } from '../config.js'; 12 | import { 13 | hasViolatingAnchorTag, 14 | hasInvalidAttributes, 15 | hasInvalidScripts, 16 | processFoundJS, 17 | scanForScripts, 18 | storeFoundJS, 19 | } from '../contentUtils.js'; 20 | 21 | describe('contentUtils', () => { 22 | beforeEach(() => { 23 | window.chrome.runtime.sendMessage = jest.fn(() => {}); 24 | }); 25 | describe('storeFoundJS', () => { 26 | it('should handle scripts with src correctly', () => { 27 | const scriptList = []; 28 | const fakeUrl = 'https://fancytestingyouhere.com/'; 29 | const fakeScriptNode = { 30 | src: fakeUrl, 31 | }; 32 | storeFoundJS(fakeScriptNode, scriptList); 33 | expect(scriptList.length).toEqual(1); 34 | expect(scriptList[0].src).toEqual(fakeUrl); 35 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(1); 36 | }); 37 | it('should handle inline scripts correctly', () => { 38 | const scriptList = []; 39 | const fakeInnerHtml = 'console.log'; 40 | const fakeLookupKey = 'somelonghashkey'; 41 | const fakeScriptNode = { 42 | attributes: { 43 | 'data-binary-transparency-hash-key': { value: fakeLookupKey }, 44 | }, 45 | innerHTML: fakeInnerHtml, 46 | }; 47 | storeFoundJS(fakeScriptNode, scriptList); 48 | expect(scriptList.length).toEqual(1); 49 | expect(scriptList[0].rawjs).toEqual(fakeInnerHtml); 50 | expect(scriptList[0].lookupKey).toEqual(fakeLookupKey); 51 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(1); 52 | }); 53 | it('should send update icon message if valid', () => { 54 | const scriptList = []; 55 | const fakeUrl = 'https://fancytestingyouhere.com/'; 56 | const fakeScriptNode = { 57 | src: fakeUrl, 58 | }; 59 | storeFoundJS(fakeScriptNode, scriptList); 60 | const sentMessage = window.chrome.runtime.sendMessage.mock.calls[0][0]; 61 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(1); 62 | expect(sentMessage.type).toEqual(MESSAGE_TYPE.UPDATE_ICON); 63 | expect(sentMessage.icon).toEqual(ICON_STATE.PROCESSING); 64 | }); 65 | it.skip('storeFoundJS keeps existing icon if not valid', () => { 66 | // TODO: come back to this after testing processFoundJS 67 | }); 68 | }); 69 | 70 | describe('hasInvalidAttributes', () => { 71 | it('should not execute if element has no attributes', () => { 72 | // no hasAttributes function 73 | let fakeElement = {}; 74 | hasInvalidAttributes(fakeElement); 75 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(0); 76 | 77 | // hasAttributes is a function, but has no attributes 78 | fakeElement = { 79 | hasAttributes: () => { 80 | return false; 81 | }, 82 | }; 83 | hasInvalidAttributes(fakeElement); 84 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(0); 85 | }); 86 | it('should not update the icon if no violating attributes are found', () => { 87 | const fakeElement = { 88 | attributes: [ 89 | { localName: 'background' }, 90 | { localName: 'height' }, 91 | { localName: 'width' }, 92 | ], 93 | hasAttributes: () => { 94 | return true; 95 | }, 96 | }; 97 | hasInvalidAttributes(fakeElement); 98 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(0); 99 | }); 100 | it('should update the icon if violating attributes are found', () => { 101 | const fakeElement = { 102 | attributes: [ 103 | { localName: 'onclick' }, 104 | { localName: 'height' }, 105 | { localName: 'width' }, 106 | ], 107 | hasAttributes: () => { 108 | return true; 109 | }, 110 | }; 111 | hasInvalidAttributes(fakeElement); 112 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(1); 113 | }); 114 | }); 115 | describe('hasViolatingAnchorTag', () => { 116 | it('should check for violating anchor tags with javascript urls', () => { 117 | const anchorTagElement = { 118 | nodeName: 'A', 119 | href: "javascript:alert('test')", 120 | }; 121 | 122 | hasViolatingAnchorTag(anchorTagElement); 123 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toEqual(2); 124 | expect(window.chrome.runtime.sendMessage.mock.calls[1][0].type).toEqual( 125 | MESSAGE_TYPE.UPDATE_ICON 126 | ); 127 | expect(window.chrome.runtime.sendMessage.mock.calls[1][0].icon).toEqual( 128 | ICON_STATE.INVALID_SOFT 129 | ); 130 | }); 131 | it('should check for violating anchor tags with javascript urls in node children', () => { 132 | const childAnchorTagElement = { 133 | nodeName: 'A', 134 | href: "javascript:alert('test')", 135 | }; 136 | const anchorTagElement = { 137 | nodeName: 'A', 138 | href: 'test.com', 139 | childNodes: [childAnchorTagElement], 140 | }; 141 | 142 | hasViolatingAnchorTag(anchorTagElement); 143 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toEqual(2); 144 | expect(window.chrome.runtime.sendMessage.mock.calls[1][0].type).toEqual( 145 | MESSAGE_TYPE.UPDATE_ICON 146 | ); 147 | expect(window.chrome.runtime.sendMessage.mock.calls[1][0].icon).toEqual( 148 | ICON_STATE.INVALID_SOFT 149 | ); 150 | }); 151 | }); 152 | describe('hasInvalidScripts', () => { 153 | it('should not check for non-HTMLElements', () => { 154 | const fakeElement = { 155 | attributes: [ 156 | { localName: 'onclick' }, 157 | { localName: 'height' }, 158 | { localName: 'width' }, 159 | ], 160 | hasAttributes: () => { 161 | return true; 162 | }, 163 | nodeType: 2, 164 | }; 165 | hasInvalidScripts(fakeElement, []); 166 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(0); 167 | }); 168 | it('should store any script elements we find', () => { 169 | const fakeElement = { 170 | attributes: { 'data-binary-transparency-hash-key': { value: 'green' } }, 171 | hasAttributes: () => { 172 | return false; 173 | }, 174 | nodeName: 'SCRIPT', 175 | nodeType: 1, 176 | }; 177 | const foundScripts = []; 178 | hasInvalidScripts(fakeElement, foundScripts); 179 | expect(foundScripts.length).toBe(1); 180 | expect(foundScripts[0].type).toBe(MESSAGE_TYPE.RAW_JS); 181 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(1); 182 | expect(window.chrome.runtime.sendMessage.mock.calls[0][0].type).toBe( 183 | MESSAGE_TYPE.UPDATE_ICON 184 | ); 185 | }); 186 | it('should check all child nodes for non script elements', () => { 187 | const fakeElement = { 188 | childNodes: [ 189 | { 190 | attributes: [ 191 | { localName: 'onclick' }, 192 | { localName: 'height' }, 193 | { localName: 'width' }, 194 | ], 195 | hasAttributes: () => { 196 | return true; 197 | }, 198 | nodeType: 2, 199 | }, 200 | { 201 | attributes: [ 202 | { localName: 'onclick' }, 203 | { localName: 'height' }, 204 | { localName: 'width' }, 205 | ], 206 | hasAttributes: () => { 207 | return true; 208 | }, 209 | nodeType: 3, 210 | }, 211 | ], 212 | hasAttributes: () => { 213 | return false; 214 | }, 215 | nodeType: 1, 216 | }; 217 | const foundScripts = []; 218 | hasInvalidScripts(fakeElement, foundScripts); 219 | expect(foundScripts.length).toBe(0); 220 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(0); 221 | }); 222 | it('should store any script element direct children', () => { 223 | const fakeElement = { 224 | childNodes: [ 225 | { 226 | attributes: [ 227 | { localName: 'onclick' }, 228 | { localName: 'height' }, 229 | { localName: 'width' }, 230 | ], 231 | hasAttributes: () => { 232 | return true; 233 | }, 234 | nodeType: 2, 235 | }, 236 | { 237 | attributes: { 238 | 'data-binary-transparency-hash-key': { value: 'green' }, 239 | }, 240 | hasAttributes: () => { 241 | return false; 242 | }, 243 | nodeName: 'SCRIPT', 244 | nodeType: 1, 245 | }, 246 | ], 247 | hasAttributes: () => { 248 | return false; 249 | }, 250 | nodeType: 1, 251 | }; 252 | const foundScripts = []; 253 | hasInvalidScripts(fakeElement, foundScripts); 254 | expect(foundScripts.length).toBe(1); 255 | expect(foundScripts[0].type).toBe(MESSAGE_TYPE.RAW_JS); 256 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(1); 257 | expect(window.chrome.runtime.sendMessage.mock.calls[0][0].type).toBe( 258 | MESSAGE_TYPE.UPDATE_ICON 259 | ); 260 | }); 261 | it('should check for any grandchildren script elements', () => { 262 | const fakeElement = { 263 | childNodes: [ 264 | { 265 | attributes: [ 266 | { localName: 'onclick' }, 267 | { localName: 'height' }, 268 | { localName: 'width' }, 269 | ], 270 | hasAttributes: () => { 271 | return true; 272 | }, 273 | nodeType: 2, 274 | }, 275 | { 276 | attributes: { 277 | 'data-binary-transparency-hash-key': { value: 'green' }, 278 | }, 279 | getElementsByTagName: () => { 280 | return [ 281 | { 282 | attributes: { 283 | 'data-binary-transparency-hash-key': { value: 'green1' }, 284 | }, 285 | }, 286 | { 287 | attributes: { 288 | 'data-binary-transparency-hash-key': { value: 'green2' }, 289 | }, 290 | }, 291 | ]; 292 | }, 293 | hasAttributes: () => { 294 | return false; 295 | }, 296 | nodeType: 1, 297 | }, 298 | ], 299 | hasAttributes: () => { 300 | return false; 301 | }, 302 | nodeType: 1, 303 | }; 304 | const foundScripts = []; 305 | hasInvalidScripts(fakeElement, foundScripts); 306 | expect(foundScripts.length).toBe(2); 307 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(2); 308 | }); 309 | }); 310 | describe('scanForScripts', () => { 311 | it('should find existing script tags in the DOM and check them', () => { 312 | jest.resetModules(); 313 | document.body.innerHTML = 314 | '
' + 315 | ' ' + 316 | ' ' + 317 | '
'; 318 | scanForScripts(); 319 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(2); 320 | }); 321 | }); 322 | describe('processFoundJS', () => { 323 | // these are flaky because jest.resestModules doesn't work for esm 324 | // while the above may be true, redo these as async and flush promises and they should work. 325 | it('should send valid icon update when no src based scripts are invalid', async () => { 326 | document.body.innerHTML = 327 | '
' + 328 | ' ' + 329 | ' ' + 330 | '
'; 331 | scanForScripts(); 332 | window.chrome.runtime.sendMessage.mockImplementation( 333 | (message, response) => { 334 | response && response({ valid: true }); 335 | } 336 | ); 337 | processFoundJS(ORIGIN_TYPE.WHATSAPP, '100'); 338 | await (() => new Promise(res => setTimeout(res, 10)))(); 339 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(9); 340 | expect(window.chrome.runtime.sendMessage.mock.calls[6][0].icon).toEqual( 341 | ICON_STATE.VALID 342 | ); 343 | }); 344 | it('should send valid icon update when no inline based scripts are invalid', async () => { 345 | document.body.innerHTML = 346 | '
' + 347 | ' ' + 348 | ' ' + 349 | '
'; 350 | scanForScripts(); 351 | window.chrome.runtime.sendMessage.mockImplementation( 352 | (message, response) => { 353 | response && response({ valid: true }); 354 | } 355 | ); 356 | processFoundJS(ORIGIN_TYPE.WHATSAPP, '102'); 357 | await (() => new Promise(res => setTimeout(res, 10)))(); 358 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(11); 359 | expect(window.chrome.runtime.sendMessage.mock.calls[6][0].icon).toEqual( 360 | ICON_STATE.VALID 361 | ); 362 | }); 363 | it('should send invalid icon update when invalid response received with src', async () => { 364 | document.body.innerHTML = 365 | '
' + 366 | ' ' + 367 | ' ' + 368 | '
'; 369 | scanForScripts(); 370 | window.chrome.runtime.sendMessage.mockImplementation( 371 | (message, response) => { 372 | response && response({ valid: false }); 373 | } 374 | ); 375 | processFoundJS(ORIGIN_TYPE.WHATSAPP, '101'); 376 | await (() => new Promise(res => setTimeout(res, 10)))(); 377 | expect(window.chrome.runtime.sendMessage.mock.calls.length).toBe(14); 378 | expect(window.chrome.runtime.sendMessage.mock.calls[7][0].icon).toEqual( 379 | ICON_STATE.INVALID_SOFT 380 | ); 381 | }); 382 | // it.todo( 383 | // 'should send invalid icon update when invalid inline response received' 384 | // ); 385 | }); 386 | }); 387 | -------------------------------------------------------------------------------- /src/js/background.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { 9 | MESSAGE_TYPE, 10 | ORIGIN_HOST, 11 | ORIGIN_TIMEOUT, 12 | ORIGIN_TYPE, 13 | } from './config.js'; 14 | const manifestCache = new Map(); 15 | const debugCache = new Map(); 16 | 17 | // Emulate PageActions 18 | chrome.runtime.onInstalled.addListener(() => { 19 | chrome.action.disable(); 20 | }); 21 | 22 | function updateIconV3(message, sender) { 23 | chrome.action.setIcon({ tabId: sender.tab.id, path: message.icon.badge }); 24 | const popupMessage = { 25 | tabId: sender.tab.id, 26 | popup: message.icon.popup, 27 | }; 28 | chrome.action.setPopup(popupMessage); 29 | const messageForPopup = { 30 | popup: message.icon.popup, 31 | tabId: sender.tab.id, 32 | }; 33 | chrome.runtime.sendMessage(messageForPopup); 34 | chrome.action.enable(sender.tab.id); 35 | } 36 | 37 | function updateIconV2(message, sender) { 38 | chrome.pageAction.setIcon({ tabId: sender.tab.id, path: message.icon.badge }); 39 | const popupMessage = { 40 | tabId: sender.tab.id, 41 | popup: message.icon.popup, 42 | }; 43 | chrome.pageAction.setPopup(popupMessage); 44 | const messageForPopup = { 45 | popup: message.icon.popup, 46 | tabId: sender.tab.id, 47 | }; 48 | chrome.runtime.sendMessage(messageForPopup); 49 | chrome.pageAction.show(sender.tab.id); 50 | } 51 | 52 | function updateIcon(message, sender) { 53 | console.log('background messages are ', message); 54 | if (chrome.pageAction) { 55 | updateIconV2(message, sender); 56 | } else { 57 | updateIconV3(message, sender); 58 | } 59 | } 60 | 61 | function addDebugLog(tabId, debugMessage) { 62 | let tabDebugList = debugCache.get(tabId); 63 | if (tabDebugList == null) { 64 | tabDebugList = []; 65 | debugCache.set(tabId, tabDebugList); 66 | } 67 | 68 | tabDebugList.push(debugMessage); 69 | } 70 | 71 | const fromHexString = hexString => 72 | new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); 73 | const toHexString = bytes => 74 | bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); 75 | 76 | function getCFHashWorkaroundFunction(host, version) { 77 | return new Promise((resolve, reject) => { 78 | fetch( 79 | 'https://staging-api.privacy-auditability.cloudflare.com/v1/hash/' + 80 | host + 81 | '/' + 82 | version, 83 | { method: 'GET' } 84 | ) 85 | .then(response => { 86 | resolve(response); 87 | }) 88 | .catch(response => { 89 | reject(response); 90 | }); 91 | }); 92 | } 93 | 94 | async function validateManifest(rootHash, leaves, host, version, workaround) { 95 | // does rootHash match what was published? 96 | const cfResponse = await getCFHashWorkaroundFunction(host, version).catch( 97 | cfError => { 98 | console.log('error fetching hash from CF', cfError); 99 | return { 100 | valid: false, 101 | reason: 'ENDPOINT_FAILURE', 102 | error: cfError, 103 | }; 104 | } 105 | ); 106 | if (cfResponse == null || cfResponse.json == null) { 107 | return { 108 | valid: false, 109 | reason: 'UNKNOWN_ENDPOINT_ISSUE', 110 | }; 111 | } 112 | const cfPayload = await cfResponse.json(); 113 | let cfRootHash = cfPayload.root_hash; 114 | if (cfPayload.root_hash.startsWith('0x')) { 115 | cfRootHash = cfPayload.root_hash.slice(2); 116 | } 117 | // validate 118 | if (rootHash !== cfRootHash) { 119 | console.log('hash mismatch with CF ', rootHash, cfRootHash); 120 | 121 | // secondary hash to mitigate accidental build issue. 122 | const encoder = new TextEncoder(); 123 | const backupHashEncoded = encoder.encode(workaround); 124 | const backupHashArray = Array.from( 125 | new Uint8Array(await crypto.subtle.digest('SHA-256', backupHashEncoded)) 126 | ); 127 | const backupHash = backupHashArray 128 | .map(b => b.toString(16).padStart(2, '0')) 129 | .join(''); 130 | console.log( 131 | 'secondary hashing of CF value fails too ', 132 | rootHash, 133 | backupHash 134 | ); 135 | if (backupHash !== cfRootHash) { 136 | return { 137 | valid: false, 138 | reason: 'ROOT_HASH_VERFIY_FAIL_3RD_PARTY', 139 | }; 140 | } 141 | } 142 | 143 | let oldhashes = leaves.map( 144 | leaf => fromHexString(leaf.replace('0x', '')).buffer 145 | ); 146 | let newhashes = []; 147 | let bonus = ''; 148 | 149 | while (oldhashes.length > 1) { 150 | for (let index = 0; index < oldhashes.length; index += 2) { 151 | const validSecondValue = index + 1 < oldhashes.length; 152 | if (validSecondValue) { 153 | const hashValue = new Uint8Array( 154 | oldhashes[index].byteLength + oldhashes[index + 1].byteLength 155 | ); 156 | hashValue.set(new Uint8Array(oldhashes[index]), 0); 157 | hashValue.set( 158 | new Uint8Array(oldhashes[index + 1]), 159 | oldhashes[index].byteLength 160 | ); 161 | newhashes.push(await crypto.subtle.digest('SHA-256', hashValue.buffer)); 162 | } else { 163 | bonus = oldhashes[index]; 164 | } 165 | } 166 | oldhashes = newhashes; 167 | if (bonus !== '') { 168 | oldhashes.push(bonus); 169 | } 170 | console.log( 171 | 'layer hex is ', 172 | oldhashes.map(hash => { 173 | return Array.from(new Uint8Array(hash)) 174 | .map(b => b.toString(16).padStart(2, '')) 175 | .join(''); 176 | }) 177 | ); 178 | newhashes = []; 179 | bonus = ''; 180 | console.log( 181 | 'in loop hashes.length is', 182 | oldhashes.length, 183 | rootHash, 184 | oldhashes 185 | ); 186 | } 187 | const lastHash = toHexString(new Uint8Array(oldhashes[0])); 188 | console.log('before return comparison', rootHash, lastHash); 189 | if (lastHash === rootHash) { 190 | return { 191 | valid: true, 192 | }; 193 | } 194 | return { 195 | valid: false, 196 | reason: 'ROOT_HASH_VERFIY_FAIL_IN_PAGE', 197 | }; 198 | } 199 | 200 | async function validateMetaCompanyManifest(rootHash, otherHashes, leaves) { 201 | // merge all the hashes into one 202 | const megaHash = JSON.stringify(leaves); 203 | // hash it 204 | const encoder = new TextEncoder(); 205 | const encodedMegaHash = encoder.encode(megaHash); 206 | const jsHashArray = Array.from( 207 | new Uint8Array(await crypto.subtle.digest('SHA-256', encodedMegaHash)) 208 | ); 209 | const jsHash = jsHashArray.map(b => b.toString(16).padStart(2, '0')).join(''); 210 | // compare to main and long tail, it should match one 211 | // then hash it with the other 212 | let combinedHash = ''; 213 | if (jsHash === otherHashes.main || jsHash === otherHashes.longtail) { 214 | const combinedHashArray = Array.from( 215 | new Uint8Array( 216 | await crypto.subtle.digest( 217 | 'SHA-256', 218 | encoder.encode(otherHashes.longtail + otherHashes.main) 219 | ) 220 | ) 221 | ); 222 | combinedHash = combinedHashArray 223 | .map(b => b.toString(16).padStart(2, '0')) 224 | .join(''); 225 | } else { 226 | return false; 227 | } 228 | 229 | // ensure result matches root, return. 230 | console.log('combined hash is ', combinedHash, rootHash); 231 | return combinedHash === rootHash; 232 | } 233 | 234 | async function processJSWithSrc(message, manifest, tabId) { 235 | try { 236 | const sourceResponse = await fetch(message.src, { method: 'GET' }); 237 | let sourceText = await sourceResponse.text(); 238 | if (sourceText.indexOf('if (self.CavalryLogger) {') === 0) { 239 | sourceText = sourceText.slice(82).trim(); 240 | } 241 | // we want to slice out the source URL from the source 242 | const sourceURLIndex = sourceText.indexOf('//# sourceURL'); 243 | if (sourceURLIndex >= 0) { 244 | // doing minus 1 because there's usually either a space or new line 245 | sourceText = sourceText.slice(0, sourceURLIndex - 1); 246 | } 247 | // if ([ORIGIN_TYPE.FACEBOOK].includes(message.origin)) { 248 | // sourceText = unescape(sourceText); 249 | // } 250 | // strip i18n delimiters 251 | // eslint-disable-next-line no-useless-escape 252 | const i18nRegexp = /\/\*FBT_CALL\*\/.*?\/\*FBT_CALL\*\//g; 253 | const i18nStripped = sourceText.replace(i18nRegexp, ''); 254 | // split package up if necessary 255 | const packages = i18nStripped.split('/*FB_PKG_DELIM*/\n'); 256 | const encoder = new TextEncoder(); 257 | for (let i = 0; i < packages.length; i++) { 258 | const encodedPackage = encoder.encode(packages[i]); 259 | const packageHashBuffer = await crypto.subtle.digest( 260 | 'SHA-256', 261 | encodedPackage 262 | ); 263 | const packageHash = Array.from(new Uint8Array(packageHashBuffer)) 264 | .map(b => b.toString(16).padStart(2, '0')) 265 | .join(''); 266 | console.log( 267 | 'manifest is ', 268 | manifest.leaves.length, 269 | manifest.leaves.includes(packageHash), 270 | packageHash 271 | ); 272 | if (!manifest.leaves.includes(packageHash)) { 273 | return false; 274 | } 275 | } 276 | return true; // YAY! 277 | } catch (error) { 278 | console.log('error occurred!', error); 279 | addDebugLog( 280 | tabId, 281 | 'Error: Processing JS with SRC ' + 282 | message.origin + 283 | ', ' + 284 | message.version + 285 | ' problematic JS is ' + 286 | message.src + 287 | 'error is ' + 288 | JSON.stringify(error).substring(0, 500) 289 | ); 290 | return false; 291 | } 292 | } 293 | 294 | // async function processRawJS() {} 295 | 296 | function getDebugLog(tabId) { 297 | let tabDebugList = debugCache.get(tabId); 298 | return tabDebugList == null ? [] : tabDebugList; 299 | } 300 | 301 | export function handleMessages(message, sender, sendResponse) { 302 | console.log('in handle messages ', message); 303 | if (message.type == MESSAGE_TYPE.UPDATE_ICON) { 304 | updateIcon(message, sender); 305 | return; 306 | } 307 | 308 | if (message.type == MESSAGE_TYPE.LOAD_MANIFEST) { 309 | // validate manifest 310 | if ([ORIGIN_TYPE.FACEBOOK].includes(message.origin)) { 311 | validateMetaCompanyManifest( 312 | message.rootHash, 313 | message.otherHashes, 314 | message.leaves 315 | ).then(valid => { 316 | console.log('result is ', valid); 317 | if (valid) { 318 | let origin = manifestCache.get(message.origin); 319 | if (origin == null) { 320 | origin = new Map(); 321 | manifestCache.set(message.origin, origin); 322 | } 323 | // roll through the existing manifests and remove expired ones 324 | if (ORIGIN_TIMEOUT[message.origin] > 0) { 325 | for (let [key, manif] of origin.entries()) { 326 | if (manif.start + ORIGIN_TIMEOUT[message.origin] < Date.now()) { 327 | origin.delete(key); 328 | } 329 | } 330 | } 331 | 332 | let manifest = origin.get(message.version); 333 | if (!manifest) { 334 | manifest = { 335 | leaves: [], 336 | root: message.rootHash, 337 | start: Date.now(), 338 | }; 339 | origin.set(message.version, manifest); 340 | } 341 | message.leaves.forEach(leaf => { 342 | if (!manifest.leaves.includes(leaf)) { 343 | manifest.leaves.push(leaf); 344 | } 345 | }); 346 | sendResponse({ valid: true }); 347 | } else { 348 | sendResponse({ valid: false }); 349 | } 350 | }); 351 | } else { 352 | const slicedHash = message.rootHash.slice(2); 353 | const slicedLeaves = message.leaves.map(leaf => { 354 | return leaf.slice(2); 355 | }); 356 | validateManifest( 357 | slicedHash, 358 | slicedLeaves, 359 | ORIGIN_HOST[message.origin], 360 | message.version, 361 | message.workaround 362 | ).then(validationResult => { 363 | if (validationResult.valid) { 364 | // store manifest to subsequently validate JS 365 | let origin = manifestCache.get(message.origin); 366 | if (origin == null) { 367 | origin = new Map(); 368 | manifestCache.set(message.origin, origin); 369 | } 370 | // roll through the existing manifests and remove expired ones 371 | if (ORIGIN_TIMEOUT[message.origin] > 0) { 372 | for (let [key, manif] of origin.entries()) { 373 | if (manif.start + ORIGIN_TIMEOUT[message.origin] < Date.now()) { 374 | origin.delete(key); 375 | } 376 | } 377 | } 378 | console.log('result is ', validationResult.valid); 379 | origin.set(message.version, { 380 | leaves: slicedLeaves, 381 | root: slicedHash, 382 | start: Date.now(), 383 | }); 384 | sendResponse({ valid: true }); 385 | } else { 386 | sendResponse(validationResult); 387 | } 388 | }); 389 | } 390 | return true; 391 | } 392 | 393 | if (message.type == MESSAGE_TYPE.JS_WITH_SRC) { 394 | // exclude known extension scripts from analysis 395 | if ( 396 | message.src.indexOf('chrome-extension://') === 0 || 397 | message.src.indexOf('moz-extension://') === 0 398 | ) { 399 | addDebugLog( 400 | sender.tab.id, 401 | 'Warning: User installed extension inserted script ' + message.src 402 | ); 403 | sendResponse({ 404 | valid: false, 405 | type: 'EXTENSION', 406 | reason: 'User installed extension has inserted script', 407 | }); 408 | return; 409 | } 410 | 411 | const origin = manifestCache.get(message.origin); 412 | if (!origin) { 413 | addDebugLog( 414 | sender.tab.id, 415 | 'Error: JS with SRC had no matching origin ' + message.origin 416 | ); 417 | sendResponse({ valid: false, reason: 'no matching origin' }); 418 | return; 419 | } 420 | const manifestObj = origin.get(message.version); 421 | const manifest = manifestObj && manifestObj.leaves; 422 | if (!manifest) { 423 | addDebugLog( 424 | sender.tab.id, 425 | 'Error: JS with SRC had no matching manifest. origin: ' + 426 | message.origin + 427 | ' version: ' + 428 | message.version 429 | ); 430 | sendResponse({ valid: false, reason: 'no matching manifest' }); 431 | return; 432 | } 433 | // fetch and process the src 434 | processJSWithSrc(message, manifestObj, sender.tab.id).then(valid => { 435 | console.log('sending processJSWithSrc response ', valid); 436 | sendResponse({ valid: valid }); 437 | }); 438 | return true; 439 | } 440 | 441 | if (message.type == MESSAGE_TYPE.RAW_JS) { 442 | const origin = manifestCache.get(message.origin); 443 | if (!origin) { 444 | addDebugLog( 445 | sender.tab.id, 446 | 'Error: RAW_JS had no matching origin ' + message.origin 447 | ); 448 | sendResponse({ valid: false, reason: 'no matching origin' }); 449 | return; 450 | } 451 | const manifestObj = origin.get(message.version); 452 | const manifest = manifestObj && manifestObj.leaves; 453 | if (!manifest) { 454 | addDebugLog( 455 | sender.tab.id, 456 | 'Error: JS with SRC had no matching manifest. origin: ' + 457 | message.origin + 458 | ' version: ' + 459 | message.version 460 | ); 461 | sendResponse({ valid: false, reason: 'no matching manifest' }); 462 | return; 463 | } 464 | 465 | // fetch the src 466 | const encoder = new TextEncoder(); 467 | const encodedJS = encoder.encode(message.rawjs); 468 | // hash the src 469 | crypto.subtle.digest('SHA-256', encodedJS).then(jsHashBuffer => { 470 | const jsHashArray = Array.from(new Uint8Array(jsHashBuffer)); 471 | const jsHash = jsHashArray 472 | .map(b => b.toString(16).padStart(2, '0')) 473 | .join(''); 474 | 475 | console.log('generate hash is ', jsHash); 476 | if (manifestObj.leaves.includes(jsHash)) { 477 | sendResponse({ valid: true }); 478 | } else { 479 | addDebugLog( 480 | sender.tab.id, 481 | 'Error: hash does not match ' + 482 | message.origin + 483 | ', ' + 484 | message.version + 485 | ', unmatched JS is ' + 486 | message.rawjs.substring(0, 500) 487 | ); 488 | sendResponse({ 489 | valid: false, 490 | hash: jsHash, 491 | reason: 492 | 'Error: hash does not match ' + 493 | message.origin + 494 | ', ' + 495 | message.version + 496 | ', unmatched JS is ' + 497 | message.rawjs, 498 | }); 499 | } 500 | }); 501 | return true; 502 | } 503 | 504 | if (message.type == MESSAGE_TYPE.DEBUG) { 505 | addDebugLog(sender.tab.id, message.log); 506 | return; 507 | } 508 | 509 | if (message.type == MESSAGE_TYPE.GET_DEBUG) { 510 | const debuglist = getDebugLog(message.tabId); 511 | console.log('debug list is ', message.tabId, debuglist); 512 | sendResponse({ valid: true, debugList: debuglist }); 513 | return; 514 | } 515 | } 516 | 517 | chrome.runtime.onMessage.addListener(handleMessages); 518 | chrome.tabs.onRemoved.addListener(tabId => { 519 | if (debugCache.has(tabId)) { 520 | debugCache.delete(tabId); 521 | } 522 | }); 523 | -------------------------------------------------------------------------------- /src/js/contentUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { 9 | ICON_STATE, 10 | KNOWN_EXTENSION_HASHES, 11 | MESSAGE_TYPE, 12 | ORIGIN_TYPE, 13 | } from './config.js'; 14 | 15 | const DOM_EVENTS = [ 16 | 'onabort', 17 | 'onactivate', 18 | 'onattribute', 19 | 'onafterprint', 20 | 'onafterscriptexecute', 21 | 'onafterupdate', 22 | 'onanimationend', 23 | 'onanimationiteration', 24 | 'onanimationstart', 25 | 'onariarequest', 26 | 'onautocomplete', 27 | 'onautocompleteerror', 28 | 'onbeforeactivate', 29 | 'onbeforecopy', 30 | 'onbeforecut', 31 | 'onbeforedeactivate', 32 | 'onbeforeeditfocus', 33 | 'onbeforepaste', 34 | 'onbeforeprint', 35 | 'onbeforescriptexecute', 36 | 'onbeforeunload', 37 | 'onbeforeupdate', 38 | 'onbegin', 39 | 'onblur', 40 | 'onbounce', 41 | 'oncancel', 42 | 'oncanplay', 43 | 'oncanplaythrough', 44 | 'oncellchange', 45 | 'onchange', 46 | 'onclick', 47 | 'onclose', 48 | 'oncommand', 49 | 'oncompassneedscalibration', 50 | 'oncontextmenu', 51 | 'oncontrolselect', 52 | 'oncopy', 53 | 'oncuechange', 54 | 'oncut', 55 | 'ondataavailable', 56 | 'ondatasetchanged', 57 | 'ondatasetcomplete', 58 | 'ondblclick', 59 | 'ondeactivate', 60 | 'ondevicelight', 61 | 'ondevicemotion', 62 | 'ondeviceorientation', 63 | 'ondeviceproximity', 64 | 'ondrag', 65 | 'ondragdrop', 66 | 'ondragend', 67 | 'ondragenter', 68 | 'ondragleave', 69 | 'ondragover', 70 | 'ondragstart', 71 | 'ondrop', 72 | 'ondurationchange', 73 | 'onemptied', 74 | 'onend', 75 | 'onended', 76 | 'onerror', 77 | 'onerrorupdate', 78 | 'onexit', 79 | 'onfilterchange', 80 | 'onfinish', 81 | 'onfocus', 82 | 'onfocusin', 83 | 'onfocusout', 84 | 'onformchange', 85 | 'onforminput', 86 | 'onfullscreenchange', 87 | 'onfullscreenerror', 88 | 'ongotpointercapture', 89 | 'onhashchange', 90 | 'onhelp', 91 | 'oninput', 92 | 'oninvalid', 93 | 'onkeydown', 94 | 'onkeypress', 95 | 'onkeyup', 96 | 'onlanguagechange', 97 | 'onlayoutcomplete', 98 | 'onload', 99 | 'onloadeddata', 100 | 'onloadedmetadata', 101 | 'onloadstart', 102 | 'onlosecapture', 103 | 'onlostpointercapture', 104 | 'onmediacomplete', 105 | 'onmediaerror', 106 | 'onmessage', 107 | 'onmousedown', 108 | 'onmouseenter', 109 | 'onmouseleave', 110 | 'onmousemove', 111 | 'onmouseout', 112 | 'onmouseover', 113 | 'onmouseup', 114 | 'onmousewheel', 115 | 'onmove', 116 | 'onmoveend', 117 | 'onmovestart', 118 | 'onmozfullscreenchange', 119 | 'onmozfullscreenerror', 120 | 'onmozpointerlockchange', 121 | 'onmozpointerlockerror', 122 | 'onmscontentzoom', 123 | 'onmsfullscreenchange', 124 | 'onmsfullscreenerror', 125 | 'onmsgesturechange', 126 | 'onmsgesturedoubletap', 127 | 'onmsgestureend', 128 | 'onmsgesturehold', 129 | 'onmsgesturestart', 130 | 'onmsgesturetap', 131 | 'onmsgotpointercapture', 132 | 'onmsinertiastart', 133 | 'onmslostpointercapture', 134 | 'onmsmanipulationstatechanged', 135 | 'onmspointercancel', 136 | 'onmspointerdown', 137 | 'onmspointerenter', 138 | 'onmspointerleave', 139 | 'onmspointermove', 140 | 'onmspointerout', 141 | 'onmspointerover', 142 | 'onmspointerup', 143 | 'onmssitemodejumplistitemremoved', 144 | 'onmsthumbnailclick', 145 | 'onoffline', 146 | 'ononline', 147 | 'onoutofsync', 148 | 'onpage', 149 | 'onpagehide', 150 | 'onpageshow', 151 | 'onpaste', 152 | 'onpause', 153 | 'onplay', 154 | 'onplaying', 155 | 'onpointercancel', 156 | 'onpointerdown', 157 | 'onpointerenter', 158 | 'onpointerleave', 159 | 'onpointerlockchange', 160 | 'onpointerlockerror', 161 | 'onpointermove', 162 | 'onpointerout', 163 | 'onpointerover', 164 | 'onpointerup', 165 | 'onpopstate', 166 | 'onprogress', 167 | 'onpropertychange', 168 | 'onratechange', 169 | 'onreadystatechange', 170 | 'onreceived', 171 | 'onrepeat', 172 | 'onreset', 173 | 'onresize', 174 | 'onresizeend', 175 | 'onresizestart', 176 | 'onresume', 177 | 'onreverse', 178 | 'onrowdelete', 179 | 'onrowenter', 180 | 'onrowexit', 181 | 'onrowinserted', 182 | 'onrowsdelete', 183 | 'onrowsenter', 184 | 'onrowsexit', 185 | 'onrowsinserted', 186 | 'onscroll', 187 | 'onsearch', 188 | 'onseek', 189 | 'onseeked', 190 | 'onseeking', 191 | 'onselect', 192 | 'onselectionchange', 193 | 'onselectstart', 194 | 'onstalled', 195 | 'onstorage', 196 | 'onstoragecommit', 197 | 'onstart', 198 | 'onstop', 199 | 'onshow', 200 | 'onsyncrestored', 201 | 'onsubmit', 202 | 'onsuspend', 203 | 'onsynchrestored', 204 | 'ontimeerror', 205 | 'ontimeupdate', 206 | 'ontrackchange', 207 | 'ontransitionend', 208 | 'ontoggle', 209 | 'onunload', 210 | 'onurlflip', 211 | 'onuserproximity', 212 | 'onvolumechange', 213 | 'onwaiting', 214 | 'onwebkitanimationend', 215 | 'onwebkitanimationiteration', 216 | 'onwebkitanimationstart', 217 | 'onwebkitfullscreenchange', 218 | 'onwebkitfullscreenerror', 219 | 'onwebkittransitionend', 220 | 'onwheel', 221 | ]; 222 | 223 | const foundScripts = new Map(); 224 | foundScripts.set('', []); 225 | let currentState = ICON_STATE.VALID; 226 | let currentOrigin = ''; 227 | let currentFilterType = ''; 228 | let manifestTimeoutID = ''; 229 | 230 | export function storeFoundJS(scriptNodeMaybe, scriptList) { 231 | // check if it's the manifest node 232 | if ( 233 | scriptNodeMaybe.id === 'binary-transparency-manifest' || 234 | scriptNodeMaybe.getAttribute('name') === 'binary-transparency-manifest' 235 | ) { 236 | if (manifestTimeoutID !== '') { 237 | clearTimeout(manifestTimeoutID); 238 | manifestTimeoutID = ''; 239 | } 240 | const rawManifest = JSON.parse(scriptNodeMaybe.innerHTML); 241 | 242 | let leaves = rawManifest.leaves; 243 | let otherHashes = ''; 244 | let otherType = ''; 245 | let roothash = rawManifest.root; 246 | let version = rawManifest.version; 247 | 248 | if ([ORIGIN_TYPE.FACEBOOK].includes(currentOrigin)) { 249 | leaves = rawManifest.manifest; 250 | otherHashes = rawManifest.manifest_hashes; 251 | otherType = scriptNodeMaybe.getAttribute('data-manifest-type'); 252 | roothash = otherHashes.combined_hash; 253 | version = scriptNodeMaybe.getAttribute('data-manifest-rev'); 254 | 255 | if (currentFilterType != '') { 256 | currentFilterType = 'BOTH'; 257 | } 258 | if (currentFilterType === '') { 259 | currentFilterType = otherType; 260 | } 261 | } 262 | // now that we know the actual version of the scripts, transfer the ones we know about. 263 | if (foundScripts.has('')) { 264 | foundScripts.set(version, foundScripts.get('')); 265 | foundScripts.delete(''); 266 | } 267 | 268 | chrome.runtime.sendMessage( 269 | { 270 | type: MESSAGE_TYPE.LOAD_MANIFEST, 271 | leaves: leaves, 272 | origin: currentOrigin, 273 | otherHashes: otherHashes, 274 | otherType: otherType, 275 | rootHash: roothash, 276 | workaround: scriptNodeMaybe.innerHTML, 277 | version: version, 278 | }, 279 | response => { 280 | chrome.runtime.sendMessage({ 281 | type: MESSAGE_TYPE.DEBUG, 282 | log: 283 | 'manifest load response is ' + response 284 | ? JSON.stringify(response).substring(0, 500) 285 | : '', 286 | }); 287 | // then start processing of it's JS 288 | if (response.valid) { 289 | window.setTimeout(() => processFoundJS(currentOrigin, version), 0); 290 | } else { 291 | if ( 292 | ['ENDPOINT_FAILURE', 'UNKNOWN_ENDPOINT_ISSUE'].includes( 293 | response.reason 294 | ) 295 | ) { 296 | currentState = ICON_STATE.WARNING_TIMEOUT; 297 | chrome.runtime.sendMessage({ 298 | type: MESSAGE_TYPE.UPDATE_ICON, 299 | icon: ICON_STATE.WARNING_TIMEOUT, 300 | }); 301 | return; 302 | } 303 | // TODO add Error state here, manifest didn't validate 304 | currentState = ICON_STATE.INVALID_SOFT; 305 | chrome.runtime.sendMessage({ 306 | type: MESSAGE_TYPE.UPDATE_ICON, 307 | icon: ICON_STATE.INVALID_SOFT, 308 | }); 309 | } 310 | } 311 | ); 312 | // TODO: start timeout to check if manifest hasn't loaded? 313 | } 314 | if ( 315 | scriptNodeMaybe.getAttribute('type') === 'application/json' || 316 | (scriptNodeMaybe.src != null && 317 | scriptNodeMaybe.src !== '' && 318 | scriptNodeMaybe.src.indexOf('blob:') === 0) 319 | ) { 320 | // ignore innocuous data. 321 | return; 322 | } 323 | // need to get the src of the JS 324 | if (scriptNodeMaybe.src != null && scriptNodeMaybe.src !== '') { 325 | if (scriptList.size === 1) { 326 | scriptList.get(scriptList.keys().next().value).push({ 327 | type: MESSAGE_TYPE.JS_WITH_SRC, 328 | src: scriptNodeMaybe.src, 329 | otherType: '', // TODO: read from DOM when available 330 | }); 331 | } 332 | } else { 333 | // no src, access innerHTML for the code 334 | const hashLookupAttribute = 335 | scriptNodeMaybe.attributes['data-binary-transparency-hash-key']; 336 | const hashLookupKey = hashLookupAttribute && hashLookupAttribute.value; 337 | if (scriptList.size === 1) { 338 | scriptList.get(scriptList.keys().next().value).push({ 339 | type: MESSAGE_TYPE.RAW_JS, 340 | rawjs: scriptNodeMaybe.innerHTML, 341 | lookupKey: hashLookupKey, 342 | otherType: '', // TODO: read from DOM when available 343 | }); 344 | } 345 | } 346 | if (currentState == ICON_STATE.VALID) { 347 | chrome.runtime.sendMessage({ 348 | type: MESSAGE_TYPE.UPDATE_ICON, 349 | icon: ICON_STATE.PROCESSING, 350 | }); 351 | } 352 | } 353 | 354 | export function hasViolatingAnchorTag(htmlElement) { 355 | if (htmlElement.nodeName === 'A' && htmlElement.href !== '') { 356 | let checkURL = htmlElement.href; 357 | // make sure anchor tags don't have javascript urls 358 | if (checkURL.indexOf('javascript:') == 0) { 359 | chrome.runtime.sendMessage({ 360 | type: MESSAGE_TYPE.DEBUG, 361 | log: 'violating attribute: javascript url in anchor tag', 362 | }); 363 | currentState = ICON_STATE.INVALID_SOFT; 364 | chrome.runtime.sendMessage({ 365 | type: MESSAGE_TYPE.UPDATE_ICON, 366 | icon: ICON_STATE.INVALID_SOFT, 367 | }); 368 | } 369 | 370 | if (typeof htmlElement.childNodes !== 'undefined') { 371 | htmlElement.childNodes.forEach(element => { 372 | hasViolatingAnchorTag(element); 373 | }); 374 | } 375 | } 376 | } 377 | 378 | export function hasInvalidAttributes(htmlElement) { 379 | if ( 380 | typeof htmlElement.hasAttributes === 'function' && 381 | htmlElement.hasAttributes() 382 | ) { 383 | Array.from(htmlElement.attributes).forEach(elementAttribute => { 384 | // check first for violating attributes 385 | if (DOM_EVENTS.indexOf(elementAttribute.localName) >= 0) { 386 | chrome.runtime.sendMessage({ 387 | type: MESSAGE_TYPE.DEBUG, 388 | log: 389 | 'violating attribute ' + 390 | elementAttribute.localName + 391 | ' from element ' + 392 | htmlElement.outerHTML, 393 | }); 394 | currentState = ICON_STATE.INVALID_SOFT; 395 | chrome.runtime.sendMessage({ 396 | type: MESSAGE_TYPE.UPDATE_ICON, 397 | icon: ICON_STATE.INVALID_SOFT, 398 | }); 399 | } 400 | }); 401 | } 402 | } 403 | 404 | export function hasInvalidScripts(scriptNodeMaybe, scriptList) { 405 | // if not an HTMLElement ignore it! 406 | if (scriptNodeMaybe.nodeType !== 1) { 407 | return false; 408 | } 409 | hasInvalidAttributes(scriptNodeMaybe); 410 | 411 | if (scriptNodeMaybe.nodeName === 'SCRIPT') { 412 | return storeFoundJS(scriptNodeMaybe, scriptList); 413 | } else if (scriptNodeMaybe.childNodes.length > 0) { 414 | scriptNodeMaybe.childNodes.forEach(childNode => { 415 | // if not an HTMLElement ignore it! 416 | if (childNode.nodeType !== 1) { 417 | return; 418 | } 419 | hasViolatingAnchorTag(childNode); 420 | hasInvalidAttributes(childNode); 421 | 422 | if (childNode.nodeName === 'SCRIPT') { 423 | storeFoundJS(childNode, scriptList); 424 | return; 425 | } 426 | 427 | Array.from(childNode.getElementsByTagName('script')).forEach( 428 | childScript => { 429 | storeFoundJS(childScript, scriptList); 430 | } 431 | ); 432 | }); 433 | } 434 | 435 | return; 436 | } 437 | 438 | export const scanForScripts = () => { 439 | const allElements = document.getElementsByTagName('*'); 440 | 441 | Array.from(allElements).forEach(allElement => { 442 | console.log('found existing scripts'); 443 | 444 | hasViolatingAnchorTag(allElement); 445 | hasInvalidAttributes(allElement); 446 | // next check for existing script elements and if they're violating 447 | if (allElement.nodeName === 'SCRIPT') { 448 | storeFoundJS(allElement, foundScripts); 449 | } 450 | }); 451 | 452 | // track any new scripts that get loaded in 453 | const scriptMutationObserver = new MutationObserver(mutationsList => { 454 | mutationsList.forEach(mutation => { 455 | if (mutation.type === 'childList') { 456 | Array.from(mutation.addedNodes).forEach(checkScript => { 457 | hasInvalidScripts(checkScript, foundScripts); 458 | }); 459 | } else if (mutation.type === 'attributes') { 460 | currentState = ICON_STATE.INVALID_SOFT; 461 | chrome.runtime.sendMessage({ 462 | type: MESSAGE_TYPE.UPDATE_ICON, 463 | icon: ICON_STATE.INVALID_SOFT, 464 | }); 465 | chrome.runtime.sendMessage({ 466 | type: MESSAGE_TYPE.DEBUG, 467 | log: 468 | 'Processed DOM mutation and invalid attribute added or changed ' + 469 | mutation.target, 470 | }); 471 | } 472 | }); 473 | }); 474 | 475 | scriptMutationObserver.observe(document.getElementsByTagName('html')[0], { 476 | attributeFilter: DOM_EVENTS, 477 | childList: true, 478 | subtree: true, 479 | }); 480 | }; 481 | 482 | export const processFoundJS = (origin, version) => { 483 | // foundScripts 484 | const fullscripts = foundScripts.get(version).splice(0); 485 | const scripts = fullscripts.filter(script => { 486 | if ( 487 | script.otherType === currentFilterType || 488 | ['BOTH', ''].includes(currentFilterType) 489 | ) { 490 | return true; 491 | } else { 492 | foundScripts.get(version).push(script); 493 | } 494 | }); 495 | let pendingScriptCount = scripts.length; 496 | scripts.forEach(script => { 497 | if (script.src) { 498 | chrome.runtime.sendMessage( 499 | { 500 | type: script.type, 501 | src: script.src, 502 | origin: origin, 503 | version: version, 504 | }, 505 | response => { 506 | pendingScriptCount--; 507 | if (response.valid) { 508 | if (pendingScriptCount == 0 && currentState == ICON_STATE.VALID) { 509 | chrome.runtime.sendMessage({ 510 | type: MESSAGE_TYPE.UPDATE_ICON, 511 | icon: ICON_STATE.VALID, 512 | }); 513 | } 514 | } else { 515 | if (response.type === 'EXTENSION') { 516 | currentState = ICON_STATE.WARNING_RISK; 517 | chrome.runtime.sendMessage({ 518 | type: MESSAGE_TYPE.UPDATE_ICON, 519 | icon: ICON_STATE.WARNING_RISK, 520 | }); 521 | } else { 522 | currentState = ICON_STATE.INVALID_SOFT; 523 | chrome.runtime.sendMessage({ 524 | type: MESSAGE_TYPE.UPDATE_ICON, 525 | icon: ICON_STATE.INVALID_SOFT, 526 | }); 527 | } 528 | } 529 | chrome.runtime.sendMessage({ 530 | type: MESSAGE_TYPE.DEBUG, 531 | log: 532 | 'processed JS with SRC, ' + 533 | script.src + 534 | ',response is ' + 535 | JSON.stringify(response).substring(0, 500), 536 | }); 537 | } 538 | ); 539 | } else { 540 | chrome.runtime.sendMessage( 541 | { 542 | type: script.type, 543 | rawjs: script.rawjs, 544 | lookupKey: script.lookupKey, 545 | origin: origin, 546 | version: version, 547 | }, 548 | response => { 549 | pendingScriptCount--; 550 | if (response.valid) { 551 | if (pendingScriptCount == 0 && currentState == ICON_STATE.VALID) { 552 | chrome.runtime.sendMessage({ 553 | type: MESSAGE_TYPE.UPDATE_ICON, 554 | icon: ICON_STATE.VALID, 555 | }); 556 | } 557 | } else { 558 | if (KNOWN_EXTENSION_HASHES.includes(response.hash)) { 559 | currentState = ICON_STATE.WARNING_RISK; 560 | chrome.runtime.sendMessage({ 561 | type: MESSAGE_TYPE.UPDATE_ICON, 562 | icon: ICON_STATE.WARNING_RISK, 563 | }); 564 | } else { 565 | currentState = ICON_STATE.INVALID_SOFT; 566 | chrome.runtime.sendMessage({ 567 | type: MESSAGE_TYPE.UPDATE_ICON, 568 | icon: ICON_STATE.INVALID_SOFT, 569 | }); 570 | } 571 | } 572 | chrome.runtime.sendMessage({ 573 | type: MESSAGE_TYPE.DEBUG, 574 | log: 575 | 'processed the RAW_JS, response is ' + 576 | response.hash + 577 | ' ' + 578 | JSON.stringify(response).substring(0, 500), 579 | }); 580 | } 581 | ); 582 | } 583 | }); 584 | window.setTimeout(() => processFoundJS(origin, version), 3000); 585 | }; 586 | 587 | export function startFor(origin) { 588 | currentOrigin = origin; 589 | scanForScripts(); 590 | manifestTimeoutID = setTimeout(() => { 591 | // Manifest failed to load, flag a warning to the user. 592 | currentState = ICON_STATE.WARNING_TIMEOUT; 593 | chrome.runtime.sendMessage({ 594 | type: MESSAGE_TYPE.UPDATE_ICON, 595 | icon: ICON_STATE.WARNING_TIMEOUT, 596 | }); 597 | }, 45000); 598 | } 599 | 600 | chrome.runtime.sendMessage({ 601 | type: MESSAGE_TYPE.UPDATE_ICON, 602 | icon: ICON_STATE.PROCESSING, 603 | }); 604 | --------------------------------------------------------------------------------