├── .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 |
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # meta-code-verify · [](/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 |
15 |
--------------------------------------------------------------------------------
/images/icon-badge.svg:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/images/error-badge.svg:
--------------------------------------------------------------------------------
1 |
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 |
15 |
--------------------------------------------------------------------------------
/images/temporary-extension-icon.svg:
--------------------------------------------------------------------------------
1 |
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 |
17 |
--------------------------------------------------------------------------------
/images/validated-badge.svg:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/images/circle-info.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/images/circle-exclamation-mark.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/images/circle-download-cta.svg:
--------------------------------------------------------------------------------
1 |
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 |
73 |
74 |
75 |
76 |
77 |
Meta Code Verify
78 |
81 |
82 |
83 |
84 |
Learn More
85 |
86 |
87 |
88 |
89 |
Download Source Code
90 |
91 |
92 |
93 |
94 |
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 |
37 |
--------------------------------------------------------------------------------
/images/error-header.svg:
--------------------------------------------------------------------------------
1 |
37 |
--------------------------------------------------------------------------------
/images/validated-header.svg:
--------------------------------------------------------------------------------
1 |
37 |
--------------------------------------------------------------------------------
/images/warning-header.svg:
--------------------------------------------------------------------------------
1 |
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 | '