eslint-plugin-chrome-extensions 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +767 -0
- package/docs/LICENSE.md +9 -0
- package/docs/SECURITY.md +3 -0
- package/package.json +85 -0
- package/src/data/apis.js +65 -0
- package/src/data/manifest.js +13 -0
- package/src/data/permissions.js +31 -0
- package/src/index.js +58 -0
- package/src/rules/no-content-style-leak.js +65 -0
- package/src/rules/no-deprecated-apis.js +60 -0
- package/src/rules/no-insecure-messaging.js +42 -0
- package/src/rules/no-invalid-permissions.js +57 -0
- package/src/rules/no-missing-permissions.js +30 -0
- package/src/rules/no-remote-code.js +141 -0
- package/src/rules/no-unused-permissions.js +39 -0
- package/src/rules/require-mv3.js +21 -0
- package/src/rules/valid-mv3-schema.js +44 -0
- package/src/rules/valid-service-worker.js +43 -0
- package/src/utils.js +78 -0
package/docs/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 🏛️ MIT License
|
|
2
|
+
|
|
3
|
+
**Copyright © 2026 [Adam Lui](https://codeberg.org/adamlui).**
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
|
package/docs/SECURITY.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-chrome-extensions",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ESLint rules for Chrome extensions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18.18.0"
|
|
8
|
+
},
|
|
9
|
+
"author": {
|
|
10
|
+
"name": "Adam Lui",
|
|
11
|
+
"email": "adam@kudoai.org",
|
|
12
|
+
"url": "https://codeberg.org/adamlui"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://codeberg.org/adamlui/eslint-plugin-chrome-extensions/#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://codeberg.org/adamlui/eslint-plugin-chrome-extensions/issues",
|
|
17
|
+
"email": "support@adamlui.com"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"funding": [
|
|
21
|
+
{
|
|
22
|
+
"type": "ko-fi",
|
|
23
|
+
"url": "https://ko-fi.com/adamlui"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"type": "paypal",
|
|
27
|
+
"url": "https://paypal.me/adamlui"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"main": "src/index.js",
|
|
31
|
+
"files": [
|
|
32
|
+
"docs/",
|
|
33
|
+
"src/"
|
|
34
|
+
],
|
|
35
|
+
"types": "src/index.d.ts",
|
|
36
|
+
"scripts": {
|
|
37
|
+
"prepare": "husky",
|
|
38
|
+
"lint": "eslint . --cache && yamllint .",
|
|
39
|
+
"lint:all": "eslint . && yamllint .",
|
|
40
|
+
"lint:fix": "eslint . --fix --cache && yamllint .",
|
|
41
|
+
"lint:fix-all": "eslint . --fix && yamllint .",
|
|
42
|
+
"test": "node tests/run.js",
|
|
43
|
+
"demo": "node demo/run.js",
|
|
44
|
+
"bump:patch": "bash utils/bump.sh patch",
|
|
45
|
+
"bump:minor": "bash utils/bump.sh minor",
|
|
46
|
+
"bump:feat": "npm run bump:minor",
|
|
47
|
+
"bump:major": "bash utils/bump.sh major"
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://codeberg.org/adamlui/eslint-plugin-chrome-extensions.git"
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"eslint",
|
|
55
|
+
"eslint-plugin",
|
|
56
|
+
"eslintplugin",
|
|
57
|
+
"chrome-extensions",
|
|
58
|
+
"chromium"
|
|
59
|
+
],
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"eslint": ">=9.0.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@e18e/eslint-plugin": "^0.8.1",
|
|
65
|
+
"@eslint/js": "^10.0.1",
|
|
66
|
+
"@eslint/json": "^2.1.0",
|
|
67
|
+
"@eslint/markdown": "^8.0.3",
|
|
68
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
69
|
+
"eslint": "^10.11.0",
|
|
70
|
+
"eslint-plugin-constructors": "^1.4.0",
|
|
71
|
+
"eslint-plugin-eslint-plugin": "^7.6.2",
|
|
72
|
+
"eslint-plugin-import-x": "^4.17.1",
|
|
73
|
+
"eslint-plugin-n": "^18.4.0",
|
|
74
|
+
"eslint-plugin-no-secrets": "^2.3.3",
|
|
75
|
+
"eslint-plugin-no-unsanitized": "^4.1.5",
|
|
76
|
+
"eslint-plugin-promise": "^7.3.0",
|
|
77
|
+
"eslint-plugin-regexp": "^3.3.1",
|
|
78
|
+
"eslint-plugin-security": "^4.2.0",
|
|
79
|
+
"husky": "^9.1.7",
|
|
80
|
+
"yamllint-js": "^0.2.4"
|
|
81
|
+
},
|
|
82
|
+
"dependencies": {
|
|
83
|
+
"globals": "^17.13.0"
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/data/apis.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const deprecatedAPIs = new Map([
|
|
2
|
+
['chrome.pageAction.show', 'chrome.action.enable'],
|
|
3
|
+
['chrome.pageAction.hide', 'chrome.action.disable'],
|
|
4
|
+
['chrome.pageAction.setIcon', 'chrome.action.setIcon'],
|
|
5
|
+
['chrome.pageAction.setTitle', 'chrome.action.setTitle'],
|
|
6
|
+
['chrome.pageAction.setPopup', 'chrome.action.setPopup'],
|
|
7
|
+
['chrome.pageAction.getTitle', 'chrome.action.getTitle'],
|
|
8
|
+
['chrome.pageAction.getPopup', 'chrome.action.getPopup'],
|
|
9
|
+
['chrome.browserAction', 'chrome.action'],
|
|
10
|
+
['chrome.pageAction', 'chrome.action'],
|
|
11
|
+
['chrome.extension.getBackgroundPage', 'chrome.runtime.getContexts'],
|
|
12
|
+
['chrome.extension.getURL', 'chrome.runtime.getURL'],
|
|
13
|
+
['chrome.extension.sendRequest', 'chrome.runtime.sendMessage'],
|
|
14
|
+
['chrome.extension.onRequest', 'chrome.runtime.onMessage'],
|
|
15
|
+
['chrome.extension.onRequestExternal', 'chrome.runtime.onMessageExternal'],
|
|
16
|
+
['chrome.extension.connect', 'chrome.runtime.connect'],
|
|
17
|
+
['chrome.extension.onConnect', 'chrome.runtime.onConnect'],
|
|
18
|
+
['chrome.extension.onConnectExternal', 'chrome.runtime.onConnectExternal'],
|
|
19
|
+
['chrome.tabs.executeScript', 'chrome.scripting.executeScript'],
|
|
20
|
+
['chrome.tabs.insertCSS', 'chrome.scripting.insertCSS'],
|
|
21
|
+
['chrome.tabs.removeCSS', 'chrome.scripting.removeCSS']
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
export const apiPermissionMap = new Map([
|
|
25
|
+
['chrome.alarms', 'alarms'],
|
|
26
|
+
['chrome.bookmarks', 'bookmarks'],
|
|
27
|
+
['chrome.browsingData', 'browsingData'],
|
|
28
|
+
['chrome.contextMenus', 'contextMenus'],
|
|
29
|
+
['chrome.cookies', 'cookies'],
|
|
30
|
+
['chrome.debugger', 'debugger'],
|
|
31
|
+
['chrome.declarativeNetRequest', 'declarativeNetRequest'],
|
|
32
|
+
['chrome.downloads', 'downloads'],
|
|
33
|
+
['chrome.history', 'history'],
|
|
34
|
+
['chrome.idle', 'idle'],
|
|
35
|
+
['chrome.management', 'management'],
|
|
36
|
+
['chrome.notifications', 'notifications'],
|
|
37
|
+
['chrome.pageCapture', 'pageCapture'],
|
|
38
|
+
['chrome.privacy', 'privacy'],
|
|
39
|
+
['chrome.proxy', 'proxy'],
|
|
40
|
+
['chrome.scripting', 'scripting'],
|
|
41
|
+
['chrome.sessions', 'sessions'],
|
|
42
|
+
['chrome.storage', 'storage'],
|
|
43
|
+
['chrome.tabCapture', 'tabCapture'],
|
|
44
|
+
['chrome.topSites', 'topSites'],
|
|
45
|
+
['chrome.webRequest', 'webRequest']
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
export const externalMessagingListeners =
|
|
49
|
+
new Set(['chrome.runtime.onMessageExternal', 'chrome.runtime.onConnectExternal'])
|
|
50
|
+
|
|
51
|
+
export function findDeprecated(api) {
|
|
52
|
+
for (const [deprecated, replacement] of deprecatedAPIs)
|
|
53
|
+
if (api == deprecated || api.startsWith(`${deprecated}.`))
|
|
54
|
+
return { deprecated, replacement }
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function findPermission(api) {
|
|
59
|
+
for (const [prefix, permission] of apiPermissionMap)
|
|
60
|
+
if (api == prefix || api.startsWith(`${prefix}.`))
|
|
61
|
+
return permission
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isExternalMessagingListener(api) { return externalMessagingListeners.has(api) }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import globals from 'globals'
|
|
2
|
+
|
|
3
|
+
export const mv2OnlyKeys = new Set(['browser_action', 'page_action'])
|
|
4
|
+
export const forbiddenCSPDirectives = [
|
|
5
|
+
`'unsafe-eval'`, `'unsafe-inline'`, `'unsafe-hashes'`, `'wasm-unsafe-eval'`, 'http://', 'https://']
|
|
6
|
+
|
|
7
|
+
export const serviceWorkerKeepaliveMinMS = 30000
|
|
8
|
+
|
|
9
|
+
const serviceWorkerGlobals = new Set(Object.keys(globals.serviceworker || {}))
|
|
10
|
+
export const domGlobals = new Set(Object.keys(globals.browser).filter(name => !serviceWorkerGlobals.has(name)))
|
|
11
|
+
|
|
12
|
+
export const remoteCodePatterns = [
|
|
13
|
+
'eval', 'new-function', 'string-timer', 'dynamic-import', 'remote-script-src', 'execute-script-url']
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const re = { url: /^https?:\/\// }
|
|
2
|
+
|
|
3
|
+
export const sensitivePermissions = new Set([
|
|
4
|
+
'accessibilityFeatures.modify', 'accessibilityFeatures.read', 'bookmarks', 'browsingData', 'clipboardRead',
|
|
5
|
+
'contentSettings', 'cookies', 'debugger', 'declarativeNetRequest', 'downloads', 'downloads.open', 'downloads.ui',
|
|
6
|
+
'geolocation', 'history', 'management', 'nativeMessaging', 'pageCapture', 'privacy', 'proxy', 'readingList',
|
|
7
|
+
'sessions', 'system.storage', 'tabCapture', 'tabGroups', 'tabs', 'topSites', 'ttsEngine',
|
|
8
|
+
'webAuthenticationProxy', 'webNavigation', 'webRequest', 'webRequestBlocking'
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
export const validPermissions = new Set([
|
|
12
|
+
'accessibilityFeatures.modify', 'accessibilityFeatures.read', 'activeTab', 'alarms', 'audio', 'background',
|
|
13
|
+
'bookmarks', 'browsingData', 'certificateProvider', 'clipboardRead', 'clipboardWrite', 'contentSettings',
|
|
14
|
+
'contextMenus', 'cookies', 'debugger', 'declarativeContent', 'declarativeNetRequest',
|
|
15
|
+
'declarativeNetRequestFeedback', 'declarativeNetRequestWithHostAccess', 'desktopCapture', 'dns', 'documentScan',
|
|
16
|
+
'downloads', 'downloads.open', 'downloads.ui', 'enterprise.deviceAttributes', 'enterprise.hardwarePlatform',
|
|
17
|
+
'enterprise.networkingAttributes', 'enterprise.platformKeys', 'favicon', 'fileBrowserHandler',
|
|
18
|
+
'fileSystemProvider', 'fontSettings', 'gcm', 'geolocation', 'history', 'identity', 'identity.email', 'idle',
|
|
19
|
+
'loginState', 'management', 'nativeMessaging', 'notifications', 'offscreen', 'pageCapture', 'platformKeys',
|
|
20
|
+
'power', 'printerProvider', 'printing', 'printingMetrics', 'privacy', 'processes', 'proxy', 'publicSuffix',
|
|
21
|
+
'readingList', 'runtime', 'scripting', 'search', 'sessions', 'sidePanel', 'storage', 'system.cpu',
|
|
22
|
+
'system.display', 'system.memory', 'system.storage', 'tabCapture', 'tabGroups', 'tabs', 'topSites', 'tts',
|
|
23
|
+
'ttsEngine', 'unlimitedStorage', 'userScripts', 'vpnProvider', 'wallpaper', 'webAuthenticationProxy',
|
|
24
|
+
'webNavigation', 'webRequest', 'webRequestBlocking'
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
export function isValidPermission(name, target = 'mv3') {
|
|
28
|
+
if (validPermissions.has(name)) return true
|
|
29
|
+
if (target == 'mv3') return false
|
|
30
|
+
return re.url.test(name) || name.startsWith('<') || name == '*://*/*'
|
|
31
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import noContentStyleLeak from './rules/no-content-style-leak.js'
|
|
2
|
+
import noDeprecatedAPIs from './rules/no-deprecated-apis.js'
|
|
3
|
+
import noInsecureMessaging from './rules/no-insecure-messaging.js'
|
|
4
|
+
import noInvalidPermissions from './rules/no-invalid-permissions.js'
|
|
5
|
+
import noMissingPermissions from './rules/no-missing-permissions.js'
|
|
6
|
+
import noRemoteCode from './rules/no-remote-code.js'
|
|
7
|
+
import noUnusedPermissions from './rules/no-unused-permissions.js'
|
|
8
|
+
import requireMV3 from './rules/require-mv3.js'
|
|
9
|
+
import validMV3schema from './rules/valid-mv3-schema.js'
|
|
10
|
+
import validServiceWorker from './rules/valid-service-worker.js'
|
|
11
|
+
|
|
12
|
+
const plugin = {
|
|
13
|
+
rules: {
|
|
14
|
+
'require-mv3': requireMV3,
|
|
15
|
+
'valid-mv3-schema': validMV3schema,
|
|
16
|
+
'no-deprecated-apis': noDeprecatedAPIs,
|
|
17
|
+
'no-remote-code': noRemoteCode,
|
|
18
|
+
'valid-service-worker': validServiceWorker,
|
|
19
|
+
'no-content-style-leak': noContentStyleLeak,
|
|
20
|
+
'no-invalid-permissions': noInvalidPermissions,
|
|
21
|
+
'no-missing-permissions': noMissingPermissions,
|
|
22
|
+
'no-unused-permissions': noUnusedPermissions,
|
|
23
|
+
'no-insecure-messaging': noInsecureMessaging
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const [ruleId, rule] of Object.entries(plugin.rules))
|
|
28
|
+
rule.meta.docs.url = `https://codeberg.org/adamlui/eslint-plugin-chrome-extensions/#${ruleId}`
|
|
29
|
+
|
|
30
|
+
plugin.configs = {
|
|
31
|
+
recommended: {
|
|
32
|
+
plugins: { 'chrome-extensions': plugin },
|
|
33
|
+
rules: {
|
|
34
|
+
'chrome-extensions/require-mv3': 'error',
|
|
35
|
+
'chrome-extensions/valid-mv3-schema': 'error',
|
|
36
|
+
'chrome-extensions/no-deprecated-apis': 'error',
|
|
37
|
+
'chrome-extensions/no-remote-code': 'error',
|
|
38
|
+
'chrome-extensions/valid-service-worker': 'error',
|
|
39
|
+
'chrome-extensions/no-invalid-permissions': 'error',
|
|
40
|
+
'chrome-extensions/no-missing-permissions': 'error',
|
|
41
|
+
'chrome-extensions/no-unused-permissions': 'error',
|
|
42
|
+
'chrome-extensions/no-insecure-messaging': 'error'
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
strict: {
|
|
46
|
+
plugins: { 'chrome-extensions': plugin },
|
|
47
|
+
rules: Object.fromEntries(Object.keys(plugin.rules)
|
|
48
|
+
.map(ruleId => [`chrome-extensions/${ruleId}`, 'error']))
|
|
49
|
+
},
|
|
50
|
+
problem: {
|
|
51
|
+
plugins: { 'chrome-extensions': plugin },
|
|
52
|
+
rules: Object.fromEntries(Object.entries(plugin.rules)
|
|
53
|
+
.filter(([, rule]) => rule.meta.type == 'problem')
|
|
54
|
+
.map(([ruleId]) => [`chrome-extensions/${ruleId}`, 'error']))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { plugin as default, plugin as chromeExtensions }
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readManifest, isContentScriptFile } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'suggestion',
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'Disallow injecting <style> into the host document from a content script',
|
|
8
|
+
category: 'Best Practices',
|
|
9
|
+
recommended: false
|
|
10
|
+
},
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
styleLeak: 'Inject styles via `chrome.scripting.insertCSS()` or `content_scripts.css` instead of appending to the host document.'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
create(context) {
|
|
18
|
+
const manifest = readManifest(context) ; if (!manifest || !isContentScriptFile(context, manifest)) return {}
|
|
19
|
+
const styleElems = new Set(), docAliases = new Set()
|
|
20
|
+
function isCreateStyle(node) {
|
|
21
|
+
return node?.type == 'CallExpression'
|
|
22
|
+
&& node.callee?.property?.name == 'createElement'
|
|
23
|
+
&& node.arguments[0]?.value == 'style'
|
|
24
|
+
}
|
|
25
|
+
function isObjectAssign(node) {
|
|
26
|
+
return node?.type == 'CallExpression'
|
|
27
|
+
&& node.callee?.type == 'MemberExpression'
|
|
28
|
+
&& node.callee.object?.name == 'Object'
|
|
29
|
+
&& node.callee.property?.name == 'assign'
|
|
30
|
+
}
|
|
31
|
+
function isDocumentRef(node) {
|
|
32
|
+
return node?.type == 'Identifier'
|
|
33
|
+
&& (node.name == 'document' || docAliases.has(node.name))
|
|
34
|
+
}
|
|
35
|
+
function isDocumentMember(node) {
|
|
36
|
+
return node?.type == 'MemberExpression' && isDocumentRef(node.object)
|
|
37
|
+
}
|
|
38
|
+
function isStyleTarget(node) {
|
|
39
|
+
if (isCreateStyle(node)) return true
|
|
40
|
+
if (node?.type == 'Identifier' && styleElems.has(node.name)) return true
|
|
41
|
+
if (isObjectAssign(node) && isCreateStyle(node.arguments[0])) return true
|
|
42
|
+
if (isObjectAssign(node)
|
|
43
|
+
&& node.arguments[0]?.type == 'Identifier'
|
|
44
|
+
&& styleElems.has(node.arguments[0].name)) return true
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
VariableDeclarator(node) {
|
|
49
|
+
if (node.id.type != 'Identifier') return
|
|
50
|
+
if (isCreateStyle(node.init)) styleElems.add(node.id.name)
|
|
51
|
+
if (node.init?.type == 'Identifier' && isDocumentRef(node.init))
|
|
52
|
+
docAliases.add(node.id.name)
|
|
53
|
+
},
|
|
54
|
+
CallExpression(node) {
|
|
55
|
+
if (node.callee.type != 'MemberExpression') return
|
|
56
|
+
const method = node.callee.property.name
|
|
57
|
+
if (method != 'append' && method != 'appendChild') return
|
|
58
|
+
if (!isDocumentMember(node.callee.object)) return
|
|
59
|
+
for (const arg of node.arguments)
|
|
60
|
+
if (isStyleTarget(arg))
|
|
61
|
+
context.report({ node: arg, messageId: 'styleLeak' })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { findDeprecated } from '../data/apis.js'
|
|
2
|
+
import { getMemberPath } from '../utils.js'
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'problem',
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'Disallow deprecated chrome.* extension API calls',
|
|
9
|
+
category: 'Possible Errors',
|
|
10
|
+
recommended: true
|
|
11
|
+
},
|
|
12
|
+
defaultOptions: [{ target: 'mv3', allowedAPIs: [] }],
|
|
13
|
+
schema: [{
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
target: {
|
|
17
|
+
enum: ['mv3', 'mv2'],
|
|
18
|
+
description: 'Chrome extension platform version whose deprecations to validate against'
|
|
19
|
+
},
|
|
20
|
+
allowedAPIs: {
|
|
21
|
+
oneOf: [
|
|
22
|
+
{ type: 'string', description: 'Single deprecated API namespace to allow' },
|
|
23
|
+
{
|
|
24
|
+
type: 'array',
|
|
25
|
+
items: { type: 'string', description: 'Deprecated API namespace to allow' },
|
|
26
|
+
description: 'List of deprecated API namespaces to allow'
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
description: 'Additional deprecated API namespaces to allow'
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
additionalProperties: false
|
|
33
|
+
}],
|
|
34
|
+
messages: {
|
|
35
|
+
deprecatedAPI: '`{{api}}` is deprecated in {{target}}. Use `{{replacement}}` instead.'
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
create(context) {
|
|
40
|
+
const opts = context.options[0] || {},
|
|
41
|
+
target = opts.target || 'mv3',
|
|
42
|
+
allowedAPIs = Array.isArray(opts.allowedAPIs) ? opts.allowedAPIs
|
|
43
|
+
: opts.allowedAPIs ? [opts.allowedAPIs] : []
|
|
44
|
+
if (target != 'mv3') return {}
|
|
45
|
+
function isAllowed(api) {
|
|
46
|
+
for (const allowed of allowedAPIs)
|
|
47
|
+
if (api == allowed || api.startsWith(`${allowed}.`)) return true
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
MemberExpression(node) {
|
|
52
|
+
if (node.parent.type == 'MemberExpression' && node.parent.object === node) return
|
|
53
|
+
const api = getMemberPath(node) ; if (!api?.startsWith('chrome.') || isAllowed(api)) return
|
|
54
|
+
const found = findDeprecated(api) ; if (!found) return
|
|
55
|
+
context.report({ node, messageId: 'deprecatedAPI',
|
|
56
|
+
data: { api: found.deprecated, replacement: found.replacement, target }})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isExternalMessagingListener } from '../data/apis.js'
|
|
2
|
+
import { getMemberPath } from '../utils.js'
|
|
3
|
+
|
|
4
|
+
const RE_SENDER_VALIDATION = /sender\.(?:id|origin)/
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'problem',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Require sender validation on external messaging + filtered tab broadcasts',
|
|
11
|
+
category: 'Possible Errors',
|
|
12
|
+
recommended: true
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
messages: {
|
|
16
|
+
unvalidatedExternal: '`{{listener}}` handler must validate `sender.<id|origin>` before processing messages.',
|
|
17
|
+
unfilteredBroadcast: '`chrome.tabs.query({})` with no filter broadcasts to every open tab - add `url` or `currentWindow`.'
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
create(context) {
|
|
22
|
+
return {
|
|
23
|
+
CallExpression(node) {
|
|
24
|
+
if (node.callee.type != 'MemberExpression') return
|
|
25
|
+
const path = getMemberPath(node.callee) ; if (!path) return
|
|
26
|
+
const listenerRoot = path.endsWith('.addListener') ? path.slice(0, -'.addListener'.length) : null
|
|
27
|
+
if (listenerRoot && isExternalMessagingListener(listenerRoot)) {
|
|
28
|
+
const handler = node.arguments[0] ; if (!handler) return
|
|
29
|
+
const body = context.sourceCode.getText(handler)
|
|
30
|
+
if (!RE_SENDER_VALIDATION.test(body))
|
|
31
|
+
context.report({ node, messageId: 'unvalidatedExternal', data: { listener: listenerRoot }})
|
|
32
|
+
}
|
|
33
|
+
if (path == 'chrome.tabs.query') {
|
|
34
|
+
const filter = node.arguments[0],
|
|
35
|
+
isEmpty = !filter || (filter.type == 'ObjectExpression' && !filter.properties.length)
|
|
36
|
+
if (isEmpty)
|
|
37
|
+
context.report({ node, messageId: 'unfilteredBroadcast' })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isValidPermission } from '../data/permissions.js'
|
|
2
|
+
import { readManifest } from '../utils.js'
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'problem',
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'Disallow invalid or unsupported manifest permission names',
|
|
9
|
+
category: 'Possible Errors',
|
|
10
|
+
recommended: true
|
|
11
|
+
},
|
|
12
|
+
defaultOptions: [{ target: 'mv3', allowedPermissions: [] }],
|
|
13
|
+
schema: [{
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
target: {
|
|
17
|
+
enum: ['mv3', 'mv2'],
|
|
18
|
+
description: 'Chrome extension platform version whose permissions to validate against'
|
|
19
|
+
},
|
|
20
|
+
allowedPermissions: {
|
|
21
|
+
oneOf: [
|
|
22
|
+
{ type: 'string', description: 'Single permission name to allow' },
|
|
23
|
+
{
|
|
24
|
+
type: 'array',
|
|
25
|
+
items: { type: 'string', description: 'Permission name to allow' },
|
|
26
|
+
description: 'List of permission names to allow'
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
description: 'Additional permission names to allow beyond the built-in list'
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
additionalProperties: false
|
|
33
|
+
}],
|
|
34
|
+
messages: {
|
|
35
|
+
invalidPermission: '`{{permission}}` is not a valid {{target}} permission.'
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
create(context) {
|
|
40
|
+
const opts = context.options[0] || {},
|
|
41
|
+
target = opts.target || 'mv3',
|
|
42
|
+
allowed = new Set(Array.isArray(opts.allowedPermissions) ? opts.allowedPermissions
|
|
43
|
+
: opts.allowedPermissions ? [opts.allowedPermissions] : []),
|
|
44
|
+
manifest = readManifest(context)
|
|
45
|
+
if (!manifest) return {}
|
|
46
|
+
const all = [...(manifest.permissions || []), ...(manifest.optional_permissions || [])]
|
|
47
|
+
return {
|
|
48
|
+
'Program:exit'() {
|
|
49
|
+
for (const permission of all) {
|
|
50
|
+
if (allowed.has(permission) || isValidPermission(permission, target)) continue
|
|
51
|
+
context.report({ loc: { line: 1, column: 0 },
|
|
52
|
+
messageId: 'invalidPermission', data: { permission, target }})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { findPermission } from '../data/apis.js'
|
|
2
|
+
import { readManifest, getMemberPath } from '../utils.js'
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'problem',
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'Flag chrome.* API calls used without a matching manifest permission',
|
|
9
|
+
category: 'Possible Errors',
|
|
10
|
+
recommended: true
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
missingPermission: '`{{api}}` requires the "{{permission}}" manifest permission.'
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
create(context) {
|
|
19
|
+
const manifest = readManifest(context) ; if (!manifest) return {}
|
|
20
|
+
const declared = new Set([...(manifest.permissions || []), ...(manifest.optional_permissions || [])])
|
|
21
|
+
return {
|
|
22
|
+
CallExpression(node) {
|
|
23
|
+
if (node.callee.type != 'MemberExpression') return
|
|
24
|
+
const api = getMemberPath(node.callee) ; if (!api) return
|
|
25
|
+
const permission = findPermission(api) ; if (!permission || declared.has(permission)) return
|
|
26
|
+
context.report({ node, messageId: 'missingPermission', data: { api, permission }})
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|