eslint-plugin-chrome-extensions 2.0.0 → 2.2.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 +510 -41
- package/docs/LICENSE.md +1 -1
- package/package.json +3 -5
- package/src/data/apis.js +52 -0
- package/src/data/deprecations.js +10 -0
- package/src/data/manifest.js +22 -11
- package/src/data/permissions.js +1 -1
- package/src/index.js +22 -1
- package/src/rules/manifest-key-order.js +3 -2
- package/src/rules/manifest-spacing.js +160 -0
- package/src/rules/no-duplicate-manifest-keys.js +36 -0
- package/src/rules/no-insecure-fetch.js +52 -0
- package/src/rules/no-insecure-messaging.js +44 -7
- package/src/rules/no-invalid-permissions.js +2 -2
- package/src/rules/no-missing-web-accessible-resources.js +51 -0
- package/src/rules/no-remote-code.js +11 -9
- package/src/rules/no-unrestricted-host-permissions.js +54 -0
- package/src/rules/no-unused-permissions.js +1 -1
- package/src/rules/require-mv3.js +2 -2
- package/src/rules/valid-manifest-icons.js +85 -0
- package/src/rules/valid-mv3-schema.js +50 -20
- package/src/rules/valid-service-worker.js +8 -3
- package/src/rules/valid-version-field.js +3 -3
- package/src/utils.js +11 -0
package/docs/LICENSE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🏛️ MIT License
|
|
2
2
|
|
|
3
|
-
**Copyright © 2026 [Adam Lui](https://codeberg.org/adamlui)
|
|
3
|
+
**Copyright © 2026 [Adam Lui](https://codeberg.org/adamlui)**
|
|
4
4
|
|
|
5
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
6
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-chrome-extensions",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "ESLint rules for Chrome extensions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
],
|
|
35
35
|
"types": "src/index.d.ts",
|
|
36
36
|
"scripts": {
|
|
37
|
-
"prepare": "husky",
|
|
38
37
|
"lint": "eslint . --cache && yamllint .",
|
|
39
38
|
"lint:all": "eslint . && yamllint .",
|
|
40
39
|
"lint:fix": "eslint . --fix --cache && yamllint .",
|
|
@@ -64,11 +63,11 @@
|
|
|
64
63
|
"devDependencies": {
|
|
65
64
|
"@e18e/eslint-plugin": "^0.8.1",
|
|
66
65
|
"@eslint/js": "^10.0.1",
|
|
67
|
-
"@eslint/json": "^2.1.
|
|
66
|
+
"@eslint/json": "^2.1.1",
|
|
68
67
|
"@eslint/markdown": "^8.0.3",
|
|
69
68
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
70
69
|
"eslint": "^10.12.0",
|
|
71
|
-
"eslint-plugin-constructors": "^1.4.
|
|
70
|
+
"eslint-plugin-constructors": "^1.4.2",
|
|
72
71
|
"eslint-plugin-eslint-plugin": "^7.6.2",
|
|
73
72
|
"eslint-plugin-import-x": "^4.17.1",
|
|
74
73
|
"eslint-plugin-n": "^18.4.1",
|
|
@@ -77,7 +76,6 @@
|
|
|
77
76
|
"eslint-plugin-promise": "^7.3.0",
|
|
78
77
|
"eslint-plugin-regexp": "^3.3.1",
|
|
79
78
|
"eslint-plugin-security": "^4.2.0",
|
|
80
|
-
"husky": "^9.1.7",
|
|
81
79
|
"yamllint-js": "^0.2.4"
|
|
82
80
|
},
|
|
83
81
|
"dependencies": {
|
package/src/data/apis.js
CHANGED
|
@@ -33,6 +33,7 @@ export const apiPermissionMap = new Map([
|
|
|
33
33
|
['chrome.history', 'history'],
|
|
34
34
|
['chrome.idle', 'idle'],
|
|
35
35
|
['chrome.management', 'management'],
|
|
36
|
+
['chrome.menus', 'menus'],
|
|
36
37
|
['chrome.notifications', 'notifications'],
|
|
37
38
|
['chrome.pageCapture', 'pageCapture'],
|
|
38
39
|
['chrome.privacy', 'privacy'],
|
|
@@ -41,10 +42,60 @@ export const apiPermissionMap = new Map([
|
|
|
41
42
|
['chrome.sessions', 'sessions'],
|
|
42
43
|
['chrome.storage', 'storage'],
|
|
43
44
|
['chrome.tabCapture', 'tabCapture'],
|
|
45
|
+
['chrome.tabs.query', 'tabs'],
|
|
44
46
|
['chrome.topSites', 'topSites'],
|
|
45
47
|
['chrome.webRequest', 'webRequest']
|
|
46
48
|
])
|
|
47
49
|
|
|
50
|
+
export const permissionlessAPIs = new Set([
|
|
51
|
+
// https://developer.chrome.com/docs/extensions/reference/api/management
|
|
52
|
+
'chrome.management.getPermissionWarningsByManifest',
|
|
53
|
+
'chrome.management.getSelf',
|
|
54
|
+
'chrome.management.uninstallSelf',
|
|
55
|
+
// https://developer.chrome.com/docs/extensions/reference/api/runtime
|
|
56
|
+
'chrome.runtime.connect',
|
|
57
|
+
'chrome.runtime.getManifest',
|
|
58
|
+
'chrome.runtime.getPlatformInfo',
|
|
59
|
+
'chrome.runtime.getURL',
|
|
60
|
+
'chrome.runtime.onBrowserUpdateAvailable', // eslint-disable-line no-secrets/no-secrets
|
|
61
|
+
'chrome.runtime.onConnect',
|
|
62
|
+
'chrome.runtime.onConnectExternal',
|
|
63
|
+
'chrome.runtime.onInstalled',
|
|
64
|
+
'chrome.runtime.onMessage',
|
|
65
|
+
'chrome.runtime.onMessageExternal',
|
|
66
|
+
'chrome.runtime.onStartup',
|
|
67
|
+
'chrome.runtime.onSuspend',
|
|
68
|
+
'chrome.runtime.onSuspendCanceled',
|
|
69
|
+
'chrome.runtime.onUpdateAvailable',
|
|
70
|
+
'chrome.runtime.openOptionsPage',
|
|
71
|
+
'chrome.runtime.reload',
|
|
72
|
+
'chrome.runtime.requestUpdateCheck',
|
|
73
|
+
'chrome.runtime.restart',
|
|
74
|
+
'chrome.runtime.restartAfterDelay',
|
|
75
|
+
'chrome.runtime.sendMessage',
|
|
76
|
+
'chrome.runtime.setUninstallURL',
|
|
77
|
+
// https://developer.chrome.com/docs/extensions/reference/api/tabs
|
|
78
|
+
'chrome.tabs.captureVisibleTab',
|
|
79
|
+
'chrome.tabs.connect',
|
|
80
|
+
'chrome.tabs.create',
|
|
81
|
+
'chrome.tabs.detectLanguage',
|
|
82
|
+
'chrome.tabs.discard',
|
|
83
|
+
'chrome.tabs.duplicate',
|
|
84
|
+
'chrome.tabs.executeScript',
|
|
85
|
+
'chrome.tabs.get',
|
|
86
|
+
'chrome.tabs.getCurrent',
|
|
87
|
+
'chrome.tabs.goBack',
|
|
88
|
+
'chrome.tabs.goForward',
|
|
89
|
+
'chrome.tabs.highlight',
|
|
90
|
+
'chrome.tabs.insertCSS',
|
|
91
|
+
'chrome.tabs.move',
|
|
92
|
+
'chrome.tabs.reload',
|
|
93
|
+
'chrome.tabs.remove',
|
|
94
|
+
'chrome.tabs.removeCSS',
|
|
95
|
+
'chrome.tabs.sendMessage',
|
|
96
|
+
'chrome.tabs.update'
|
|
97
|
+
])
|
|
98
|
+
|
|
48
99
|
export const externalMessagingListeners =
|
|
49
100
|
new Set(['chrome.runtime.onMessageExternal', 'chrome.runtime.onConnectExternal'])
|
|
50
101
|
|
|
@@ -56,6 +107,7 @@ export function findDeprecated(api) {
|
|
|
56
107
|
}
|
|
57
108
|
|
|
58
109
|
export function findPermission(api) {
|
|
110
|
+
if (permissionlessAPIs.has(api)) return null
|
|
59
111
|
for (const [prefix, permission] of apiPermissionMap)
|
|
60
112
|
if (api == prefix || api.startsWith(`${prefix}.`))
|
|
61
113
|
return permission
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Delete a line to end support
|
|
2
|
+
|
|
3
|
+
export const bundles = {
|
|
4
|
+
'no-invalid-permissions': ['recommended'], // 2026-10-5 announced future removal
|
|
5
|
+
'require-mv3': ['recommended'], // 2026-10-5 announced future removal
|
|
6
|
+
'valid-mv3-schema': ['recommended'], // 2026-10-5 announced future removal
|
|
7
|
+
'valid-version-field': ['recommended'] // 2026-10-5 announced future removal
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default { bundles }
|
package/src/data/manifest.js
CHANGED
|
@@ -11,19 +11,30 @@ export const manifestKeyOrder = [
|
|
|
11
11
|
'manifest_version', 'name', 'version',
|
|
12
12
|
// CWS-required
|
|
13
13
|
'description', 'icons',
|
|
14
|
-
// optional
|
|
15
|
-
'action', 'background', 'chrome_settings_overrides', 'chrome_url_overrides',
|
|
16
|
-
'commands', 'content_scripts', 'content_security_policy',
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'short_name', 'side_panel', 'storage', 'tts_engine', 'update_url',
|
|
24
|
-
'version_name', 'web_accessible_resources'
|
|
14
|
+
// optional
|
|
15
|
+
'action', 'author', 'background', 'browser_specific_settings', 'chrome_settings_overrides', 'chrome_url_overrides',
|
|
16
|
+
'commands', 'content_scripts', 'content_security_policy', 'cross_origin_embedder_policy',
|
|
17
|
+
'cross_origin_opener_policy', 'dark_theme', 'declarative_net_request', 'default_locale', 'developer',
|
|
18
|
+
'devtools_page', 'dictionaries', 'export', 'externally_connectable', 'homepage_url', 'host_permissions', 'import',
|
|
19
|
+
'incognito', 'key', 'mime_types_handler', 'minimum_chrome_version', 'oauth2', 'omnibox',
|
|
20
|
+
'optional_host_permissions', 'optional_permissions', 'options_page', 'options_ui', 'permissions',
|
|
21
|
+
'protocol_handlers', 'requirements', 'sandbox', 'short_name', 'sidebar_action', 'side_panel', 'storage', 'theme',
|
|
22
|
+
'theme_experiment', 'tts_engine', 'update_url', 'version_name', 'web_accessible_resources'
|
|
25
23
|
]
|
|
26
24
|
|
|
25
|
+
export const ffDataCollectionRequired = new Set([
|
|
26
|
+
'none', 'authenticationInfo', 'bookmarksInfo', 'browsingActivity', 'financialAndPaymentInfo', 'healthInfo',
|
|
27
|
+
'locationInfo', 'personalCommunications', 'personallyIdentifyingInfo', 'searchTerms', 'websiteActivity',
|
|
28
|
+
'websiteContent'
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
export const ffDataCollectionOptional = new Set([
|
|
32
|
+
'authenticationInfo', 'bookmarksInfo', 'browsingActivity', 'financialAndPaymentInfo', 'healthInfo', 'locationInfo',
|
|
33
|
+
'personalCommunications', 'personallyIdentifyingInfo', 'searchTerms', 'technicalAndInteraction', 'websiteActivity',
|
|
34
|
+
'websiteContent'
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
export const ffMV3bgKeys = new Set(['scripts', 'page'])
|
|
27
38
|
export const mv2OnlyKeys = new Set(['browser_action', 'page_action'])
|
|
28
39
|
|
|
29
40
|
export const remoteCodePatterns = [
|
package/src/data/permissions.js
CHANGED
|
@@ -16,7 +16,7 @@ export const validPermissions = new Set([
|
|
|
16
16
|
'downloads', 'downloads.open', 'downloads.ui', 'enterprise.deviceAttributes', 'enterprise.hardwarePlatform',
|
|
17
17
|
'enterprise.networkingAttributes', 'enterprise.platformKeys', 'favicon', 'fileBrowserHandler',
|
|
18
18
|
'fileSystemProvider', 'fontSettings', 'gcm', 'geolocation', 'history', 'identity', 'identity.email', 'idle',
|
|
19
|
-
'loginState', 'management', 'nativeMessaging', 'notifications', 'offscreen', 'pageCapture', 'platformKeys',
|
|
19
|
+
'loginState', 'management', 'menus', 'nativeMessaging', 'notifications', 'offscreen', 'pageCapture', 'platformKeys',
|
|
20
20
|
'power', 'printerProvider', 'printing', 'printingMetrics', 'privacy', 'processes', 'proxy', 'publicSuffix',
|
|
21
21
|
'readingList', 'runtime', 'scripting', 'search', 'sessions', 'sidePanel', 'storage', 'system.cpu',
|
|
22
22
|
'system.display', 'system.memory', 'system.storage', 'tabCapture', 'tabGroups', 'tabs', 'topSites', 'tts',
|
package/src/index.js
CHANGED
|
@@ -1,29 +1,43 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bundles as oldBundles } from './data/deprecations.js'
|
|
2
2
|
|
|
3
3
|
import manifestKeyOrder from './rules/manifest-key-order.js'
|
|
4
|
+
import manifestSpacing from './rules/manifest-spacing.js'
|
|
4
5
|
import noContentStyleLeak from './rules/no-content-style-leak.js'
|
|
5
6
|
import noDeprecatedAPIs from './rules/no-deprecated-apis.js'
|
|
7
|
+
import noDuplicateManifestKeys from './rules/no-duplicate-manifest-keys.js'
|
|
8
|
+
import noInsecureFetch from './rules/no-insecure-fetch.js'
|
|
6
9
|
import noInsecureMessaging from './rules/no-insecure-messaging.js'
|
|
7
10
|
import noInvalidPermissions from './rules/no-invalid-permissions.js'
|
|
8
11
|
import noMissingPermissions from './rules/no-missing-permissions.js'
|
|
12
|
+
import noMissingWebAccessibleResources from './rules/no-missing-web-accessible-resources.js'
|
|
9
13
|
import noRemoteCode from './rules/no-remote-code.js'
|
|
14
|
+
import noUnrestrictedHostPermissions from './rules/no-unrestricted-host-permissions.js'
|
|
10
15
|
import noUnusedPermissions from './rules/no-unused-permissions.js'
|
|
11
16
|
import requireMV3 from './rules/require-mv3.js'
|
|
17
|
+
import validManifestIcons from './rules/valid-manifest-icons.js'
|
|
12
18
|
import validMV3schema from './rules/valid-mv3-schema.js'
|
|
13
19
|
import validServiceWorker from './rules/valid-service-worker.js'
|
|
14
20
|
import validVersionField from './rules/valid-version-field.js'
|
|
15
21
|
|
|
22
|
+
import { bundleRules, initBundleDeprecation } from './utils.js'
|
|
23
|
+
|
|
16
24
|
const plugin = {
|
|
17
25
|
rules: {
|
|
18
26
|
'manifest-key-order': manifestKeyOrder,
|
|
27
|
+
'manifest-spacing': manifestSpacing,
|
|
19
28
|
'no-content-style-leak': noContentStyleLeak,
|
|
20
29
|
'no-deprecated-apis': noDeprecatedAPIs,
|
|
30
|
+
'no-duplicate-manifest-keys': noDuplicateManifestKeys,
|
|
31
|
+
'no-insecure-fetch': noInsecureFetch,
|
|
21
32
|
'no-insecure-messaging': noInsecureMessaging,
|
|
22
33
|
'no-invalid-permissions': noInvalidPermissions,
|
|
23
34
|
'no-missing-permissions': noMissingPermissions,
|
|
35
|
+
'no-missing-web-accessible-resources': noMissingWebAccessibleResources,
|
|
24
36
|
'no-remote-code': noRemoteCode,
|
|
37
|
+
'no-unrestricted-host-permissions': noUnrestrictedHostPermissions,
|
|
25
38
|
'no-unused-permissions': noUnusedPermissions,
|
|
26
39
|
'require-mv3': requireMV3,
|
|
40
|
+
'valid-manifest-icons': validManifestIcons,
|
|
27
41
|
'valid-mv3-schema': validMV3schema,
|
|
28
42
|
'valid-service-worker': validServiceWorker,
|
|
29
43
|
'valid-version-field': validVersionField
|
|
@@ -46,10 +60,17 @@ plugin.configs = {
|
|
|
46
60
|
plugins: { 'chrome-extensions': plugin },
|
|
47
61
|
rules: bundleRules(plugin.rules, rule => rule.meta.type == 'problem', 'chrome-extensions')
|
|
48
62
|
},
|
|
63
|
+
security: {
|
|
64
|
+
plugins: { 'chrome-extensions': plugin },
|
|
65
|
+
rules: bundleRules(plugin.rules, rule => rule.meta.docs.security, 'chrome-extensions')
|
|
66
|
+
},
|
|
49
67
|
manifest: {
|
|
50
68
|
plugins: { 'chrome-extensions': plugin },
|
|
51
69
|
rules: bundleRules(plugin.rules, rule => rule.meta.docs.manifest, 'chrome-extensions')
|
|
52
70
|
}
|
|
53
71
|
}
|
|
54
72
|
|
|
73
|
+
for (const entry of Object.entries(oldBundles))
|
|
74
|
+
initBundleDeprecation(plugin, entry, 'chrome-extensions')
|
|
75
|
+
|
|
55
76
|
export { plugin as default, plugin as chromeExtensions }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { manifestKeyOrder } from '../data/manifest.js'
|
|
2
|
+
|
|
2
3
|
import { getFilename } from '../utils.js'
|
|
3
4
|
|
|
4
5
|
const re = { manifestFilename: /manifest\.json$/ }
|
|
@@ -23,7 +24,7 @@ export default {
|
|
|
23
24
|
meta: {
|
|
24
25
|
type: 'layout',
|
|
25
26
|
docs: {
|
|
26
|
-
description: '
|
|
27
|
+
description: 'Enforces a preferred order for manifest keys',
|
|
27
28
|
category: 'Stylistic Issues',
|
|
28
29
|
recommended: false,
|
|
29
30
|
manifest: true
|
|
@@ -40,7 +41,7 @@ export default {
|
|
|
40
41
|
additionalProperties: false
|
|
41
42
|
}],
|
|
42
43
|
defaultOptions: [{ order: [] }],
|
|
43
|
-
messages: { wrongOrder: '`{{first}}` should appear before `{{second}}
|
|
44
|
+
messages: { wrongOrder: '`{{first}}` should appear before `{{second}}`.' }
|
|
44
45
|
},
|
|
45
46
|
|
|
46
47
|
create(context) {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { getFilename } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const re = { manifestFilename: /manifest\.json$/, leadingWhitespace: /^[ \t]+/ },
|
|
4
|
+
brackets = { open: new Set(['LBrace', 'LBracket']), close: new Set(['RBrace', 'RBracket']) }
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'layout',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Enforces consistent indentation and spacing in `manifest.json`',
|
|
11
|
+
category: 'Stylistic Issues',
|
|
12
|
+
recommended: false,
|
|
13
|
+
manifest: true
|
|
14
|
+
},
|
|
15
|
+
fixable: 'whitespace',
|
|
16
|
+
schema: [{
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
spaces: {
|
|
20
|
+
type: 'number',
|
|
21
|
+
description: 'Number of spaces per indent level (default 2)'
|
|
22
|
+
},
|
|
23
|
+
checkColons: {
|
|
24
|
+
type: 'boolean',
|
|
25
|
+
description: 'Enforce no spaces before and 1 space after `:` in key/value pairs (default true)'
|
|
26
|
+
},
|
|
27
|
+
checkCommas: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
description: 'Enforce no spaces before and 1 space after `,` in inline objects (default true)'
|
|
30
|
+
},
|
|
31
|
+
checkBraces: {
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
description: 'Enforce 1 space after `{` and before `}` in inline objects (default true)'
|
|
34
|
+
},
|
|
35
|
+
checkBrackets: {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
description: 'Enforce no spaces after `[` or before `]` in inline arrays (default true)'
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
additionalProperties: false
|
|
41
|
+
}],
|
|
42
|
+
defaultOptions: [{ spaces: 2, checkColons: true, checkCommas: true,
|
|
43
|
+
checkBraces: true, checkBrackets: true }],
|
|
44
|
+
messages: {
|
|
45
|
+
wrongIndent: 'Expected {{expected}} spaces of indentation (found {{found}}).',
|
|
46
|
+
tabFound: 'Expected spaces for indentation, found tabs.',
|
|
47
|
+
spaceBeforeColon: 'Expected no spaces before `:`.',
|
|
48
|
+
spaceAfterColon: 'Expected 1 space after `:`.',
|
|
49
|
+
spaceBeforeComma: 'Expected no spaces before `,`.',
|
|
50
|
+
spaceAfterComma: 'Expected 1 space after `,`.',
|
|
51
|
+
spaceAfterBrace: 'Expected 1 space after `{`.',
|
|
52
|
+
spaceBeforeBrace: 'Expected 1 space before `}`.',
|
|
53
|
+
spaceInEmptyBraces: 'Expected no spaces in empty `{}`.',
|
|
54
|
+
spaceAfterBracket: 'Expected no spaces after `[`.',
|
|
55
|
+
spaceBeforeBracket: 'Expected no spaces before `]`.'
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
create(context) {
|
|
60
|
+
if (!re.manifestFilename.test(getFilename(context))) return {}
|
|
61
|
+
const opts = context.options[0] || {},
|
|
62
|
+
spaces = opts.spaces ?? 2,
|
|
63
|
+
checkColons = opts.checkColons ?? true,
|
|
64
|
+
checkCommas = opts.checkCommas ?? true,
|
|
65
|
+
checkBraces = opts.checkBraces ?? true,
|
|
66
|
+
checkBrackets = opts.checkBrackets ?? true
|
|
67
|
+
return {
|
|
68
|
+
'Document'(document) {
|
|
69
|
+
const sourceCode = context.sourceCode,
|
|
70
|
+
text = sourceCode.getText(),
|
|
71
|
+
lines = text.split('\n'),
|
|
72
|
+
tokens = document.tokens || [],
|
|
73
|
+
lineOffsets = [0]
|
|
74
|
+
for (const line of lines)
|
|
75
|
+
lineOffsets.push(lineOffsets.at(-1) + line.length +1)
|
|
76
|
+
let depth = 0, lastLine = 0
|
|
77
|
+
for (const token of tokens) { // check indentation
|
|
78
|
+
const startLine = token.loc.start.line
|
|
79
|
+
if (startLine != lastLine) {
|
|
80
|
+
const indent = lines[startLine -1].match(re.leadingWhitespace)?.[0] || '',
|
|
81
|
+
effectiveDepth = brackets.close.has(token.type) ? Math.max(0, depth -1) : depth,
|
|
82
|
+
expected = ' '.repeat(effectiveDepth * spaces)
|
|
83
|
+
if (indent != expected) {
|
|
84
|
+
const lineStart = lineOffsets[startLine -1],
|
|
85
|
+
isTab = indent.includes('\t')
|
|
86
|
+
context.report({
|
|
87
|
+
loc: { line: startLine, column: 1 }, messageId: isTab ? 'tabFound' : 'wrongIndent',
|
|
88
|
+
data: { expected: effectiveDepth * spaces, found: indent.length },
|
|
89
|
+
fix(fixer) {
|
|
90
|
+
return fixer.replaceTextRange([lineStart, lineStart + indent.length], expected) }
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
lastLine = startLine
|
|
94
|
+
}
|
|
95
|
+
if (brackets.open.has(token.type)) depth += 1
|
|
96
|
+
else if (brackets.close.has(token.type)) depth -= 1
|
|
97
|
+
}
|
|
98
|
+
for (let t = 0 ; t < tokens.length ; t++) { // check inline spacing
|
|
99
|
+
const token = tokens[t], // eslint-disable-line security/detect-object-injection
|
|
100
|
+
type = token.type,
|
|
101
|
+
prev = tokens[t -1], next = tokens[t +1]
|
|
102
|
+
if (next) {
|
|
103
|
+
const between = text.slice(token.range[1], next.range[0]),
|
|
104
|
+
inline = !between.includes('\n'),
|
|
105
|
+
isColon = type == 'Colon', isComma = type == 'Comma'
|
|
106
|
+
if (checkColons && isColon && inline && between != ' ')
|
|
107
|
+
context.report({
|
|
108
|
+
loc: token.loc.end, messageId: 'spaceAfterColon',
|
|
109
|
+
fix(fixer) { return fixer.replaceTextRange([token.range[1], next.range[0]], ' ') }
|
|
110
|
+
})
|
|
111
|
+
if (checkCommas && isComma && inline && between != ' ')
|
|
112
|
+
context.report({
|
|
113
|
+
loc: token.loc.end, messageId: 'spaceAfterComma',
|
|
114
|
+
fix(fixer) { return fixer.replaceTextRange([token.range[1], next.range[0]], ' ') }
|
|
115
|
+
})
|
|
116
|
+
if (checkBraces && type == 'LBrace' && inline) {
|
|
117
|
+
const empty = next.type == 'RBrace', want = empty ? '' : ' '
|
|
118
|
+
if (between != want)
|
|
119
|
+
context.report({
|
|
120
|
+
loc: token.loc.end,
|
|
121
|
+
messageId: empty ? 'spaceInEmptyBraces' : 'spaceAfterBrace',
|
|
122
|
+
fix(fixer) { return fixer.replaceTextRange([token.range[1], next.range[0]], want) }
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
if (checkBrackets && type == 'LBracket' && inline && between != '')
|
|
126
|
+
context.report({
|
|
127
|
+
loc: token.loc.end, messageId: 'spaceAfterBracket',
|
|
128
|
+
fix(fixer) { return fixer.replaceTextRange([token.range[1], next.range[0]], '') }
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
if (prev) {
|
|
132
|
+
const between = text.slice(prev.range[1], token.range[0]),
|
|
133
|
+
inline = !between.includes('\n'),
|
|
134
|
+
isColon = type == 'Colon', isComma = type == 'Comma'
|
|
135
|
+
if (checkColons && isColon && inline && between != '')
|
|
136
|
+
context.report({
|
|
137
|
+
loc: token.loc.start, messageId: 'spaceBeforeColon',
|
|
138
|
+
fix(fixer) { return fixer.replaceTextRange([prev.range[1], token.range[0]], '') }
|
|
139
|
+
})
|
|
140
|
+
if (checkCommas && isComma && inline && between != '')
|
|
141
|
+
context.report({
|
|
142
|
+
loc: token.loc.start, messageId: 'spaceBeforeComma',
|
|
143
|
+
fix(fixer) { return fixer.replaceTextRange([prev.range[1], token.range[0]], '') }
|
|
144
|
+
})
|
|
145
|
+
if (checkBraces && type == 'RBrace' && prev.type != 'LBrace' && inline && between != ' ')
|
|
146
|
+
context.report({
|
|
147
|
+
loc: token.loc.start, messageId: 'spaceBeforeBrace',
|
|
148
|
+
fix(fixer) { return fixer.replaceTextRange([prev.range[1], token.range[0]], ' ') }
|
|
149
|
+
})
|
|
150
|
+
if (checkBrackets && type == 'RBracket' && prev.type != 'LBracket' && inline && between != '')
|
|
151
|
+
context.report({
|
|
152
|
+
loc: token.loc.start, messageId: 'spaceBeforeBracket',
|
|
153
|
+
fix(fixer) { return fixer.replaceTextRange([prev.range[1], token.range[0]], '') }
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getFilename } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const re = { manifestFilename: /manifest\.json$/ }
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'problem',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Disallow duplicate keys in manifest.json',
|
|
10
|
+
category: 'Possible Errors',
|
|
11
|
+
recommended: false,
|
|
12
|
+
manifest: true
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
messages: {
|
|
16
|
+
duplicateKey: 'Duplicate manifest key "{{key}}".'
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
create(context) {
|
|
21
|
+
if (!re.manifestFilename.test(getFilename(context))) return {}
|
|
22
|
+
return {
|
|
23
|
+
Object(node) {
|
|
24
|
+
const members = node.members || node.properties || [],
|
|
25
|
+
seen = new Set()
|
|
26
|
+
for (const member of members) {
|
|
27
|
+
const keyNode = member.name || member.key ; if (!keyNode) continue
|
|
28
|
+
const keyName = keyNode.value ?? keyNode.name ; if (typeof keyName != 'string') continue
|
|
29
|
+
if (seen.has(keyName))
|
|
30
|
+
context.report({ node: keyNode, messageId: 'duplicateKey', data: { key: keyName } })
|
|
31
|
+
else seen.add(keyName)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const re = { insecureURL: /^http:\/\/(?!localhost\b|127\.0\.0\.1\b|\[::1\])/i }
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem',
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'Flags `fetch()` and `XMLHttpRequest.open()` calls that use `http://`',
|
|
8
|
+
category: 'Possible Errors',
|
|
9
|
+
recommended: true,
|
|
10
|
+
security: true
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
insecureFetch: '`fetch()` to "{{url}}" insecure. Use HTTPS or a relative path instead.',
|
|
15
|
+
insecureXHR: '`XMLHttpRequest.open()` to "{{url}}" insecure. Use HTTPS or a relative path instead.'
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
create(context) {
|
|
20
|
+
const xhrVars = new Set()
|
|
21
|
+
function urlFromLiteral(node) {
|
|
22
|
+
return ( node?.type == 'Literal'
|
|
23
|
+
&& typeof node.value == 'string'
|
|
24
|
+
&& re.insecureURL.test(node.value)
|
|
25
|
+
) ? node.value : null
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
VariableDeclarator(node) {
|
|
29
|
+
if (node.init?.type == 'NewExpression'
|
|
30
|
+
&& node.init.callee?.name == 'XMLHttpRequest'
|
|
31
|
+
&& node.id.type == 'Identifier'
|
|
32
|
+
) xhrVars.add(node.id.name)
|
|
33
|
+
},
|
|
34
|
+
CallExpression(node) {
|
|
35
|
+
if (node.callee.type == 'Identifier' && node.callee.name == 'fetch') {
|
|
36
|
+
const url = urlFromLiteral(node.arguments[0])
|
|
37
|
+
if (url)
|
|
38
|
+
context.report({ node: node.arguments[0], messageId: 'insecureFetch', data: { url }})
|
|
39
|
+
}
|
|
40
|
+
if (node.callee.type == 'MemberExpression'
|
|
41
|
+
&& node.callee.property?.name == 'open'
|
|
42
|
+
&& node.callee.object?.type == 'Identifier'
|
|
43
|
+
&& xhrVars.has(node.callee.object.name)
|
|
44
|
+
) {
|
|
45
|
+
const url = urlFromLiteral(node.arguments[1])
|
|
46
|
+
if (url)
|
|
47
|
+
context.report({ node: node.arguments[1], messageId: 'insecureXHR', data: { url }})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1,24 +1,46 @@
|
|
|
1
|
-
import { isExternalMessagingListener } from '../data/apis.js'
|
|
2
1
|
import { getMemberPath } from '../utils.js'
|
|
2
|
+
import { isExternalMessagingListener } from '../data/apis.js'
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const re = { postmsgValidation: /\b\w+\.(?:origin|source)\b/, senderValidation: /sender\.(?:id|origin)/ }
|
|
5
5
|
|
|
6
6
|
export default {
|
|
7
7
|
meta: {
|
|
8
8
|
type: 'problem',
|
|
9
9
|
docs: {
|
|
10
|
-
description: '
|
|
10
|
+
description: 'Requires sender/origin validation + filtered `tabs.query()` + explicit `window.postMessage()` target',
|
|
11
11
|
category: 'Possible Errors',
|
|
12
|
-
recommended: true
|
|
12
|
+
recommended: true,
|
|
13
|
+
security: true
|
|
13
14
|
},
|
|
14
|
-
|
|
15
|
+
defaultOptions: [{ disallowWildcardTarget: true }],
|
|
16
|
+
schema: [{
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
disallowWildcardTarget: {
|
|
20
|
+
type: 'boolean',
|
|
21
|
+
description: 'Flag postMessage calls that use "*" as targetOrigin'
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
additionalProperties: false
|
|
25
|
+
}],
|
|
15
26
|
messages: {
|
|
16
27
|
unvalidatedExternal: '`{{listener}}` handler must validate `sender.<id|origin>` before processing messages.',
|
|
17
|
-
unfilteredBroadcast: '`chrome.tabs.query({})`
|
|
28
|
+
unfilteredBroadcast: '`chrome.tabs.query({})` w/ no filter broadcasts to every open tab. Add `url` or `currentWindow` instead.',
|
|
29
|
+
unvalidatedPostMessage: '`{{target}}` message handler must validate `event.origin`/`event.source` before processing messages.',
|
|
30
|
+
wildcardTarget: '`postMessage` to `"*"` leaks data to any origin. Explicitly specify `targetOrigin` instead.'
|
|
18
31
|
}
|
|
19
32
|
},
|
|
20
33
|
|
|
21
34
|
create(context) {
|
|
35
|
+
const opts = context.options[0] || {},
|
|
36
|
+
disallowWildcard = opts.disallowWildcardTarget !== false
|
|
37
|
+
function checkPostMessageHandler(node, handlerNode, target) {
|
|
38
|
+
if (!handlerNode) return
|
|
39
|
+
const body = context.sourceCode.getText(handlerNode)
|
|
40
|
+
if (re.postmsgValidation.test(body)) return
|
|
41
|
+
context.report({ node, messageId: 'unvalidatedPostMessage', data: { target }})
|
|
42
|
+
}
|
|
43
|
+
function isMessageTarget(obj) { return obj?.type == 'Identifier' && ['self', 'window'].includes(obj.name) }
|
|
22
44
|
return {
|
|
23
45
|
CallExpression(node) {
|
|
24
46
|
if (node.callee.type != 'MemberExpression') return
|
|
@@ -27,7 +49,7 @@ export default {
|
|
|
27
49
|
if (listenerRoot && isExternalMessagingListener(listenerRoot)) {
|
|
28
50
|
const handler = node.arguments[0] ; if (!handler) return
|
|
29
51
|
const body = context.sourceCode.getText(handler)
|
|
30
|
-
if (!
|
|
52
|
+
if (!re.senderValidation.test(body))
|
|
31
53
|
context.report({ node, messageId: 'unvalidatedExternal', data: { listener: listenerRoot }})
|
|
32
54
|
}
|
|
33
55
|
if (path == 'chrome.tabs.query') {
|
|
@@ -36,6 +58,21 @@ export default {
|
|
|
36
58
|
if (isEmpty)
|
|
37
59
|
context.report({ node, messageId: 'unfilteredBroadcast' })
|
|
38
60
|
}
|
|
61
|
+
if (node.callee.property?.name == 'addEventListener'
|
|
62
|
+
&& node.arguments[0]?.value == 'message'
|
|
63
|
+
&& isMessageTarget(node.callee.object)
|
|
64
|
+
) checkPostMessageHandler(node, node.arguments[1], node.callee.object.name)
|
|
65
|
+
if (disallowWildcard && node.callee.property?.name == 'postMessage') {
|
|
66
|
+
const target = node.arguments[1]
|
|
67
|
+
if (target?.type == 'Literal' && target.value == '*')
|
|
68
|
+
context.report({ node: target, messageId: 'wildcardTarget' })
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
AssignmentExpression(node) {
|
|
72
|
+
if (node.left.type == 'MemberExpression'
|
|
73
|
+
&& node.left.property?.name == 'onmessage'
|
|
74
|
+
&& isMessageTarget(node.left.object)
|
|
75
|
+
) checkPostMessageHandler(node, node.right, node.left.object.name)
|
|
39
76
|
}
|
|
40
77
|
}
|
|
41
78
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isValidPermission } from '../data/permissions.js'
|
|
2
1
|
import { getFilename, readManifest } from '../utils.js'
|
|
2
|
+
import { isValidPermission } from '../data/permissions.js'
|
|
3
3
|
|
|
4
4
|
const re = { manifestFilename: /manifest\.json$/ }
|
|
5
5
|
|
|
@@ -9,7 +9,7 @@ export default {
|
|
|
9
9
|
docs: {
|
|
10
10
|
description: 'Disallow invalid or unsupported manifest permission names',
|
|
11
11
|
category: 'Possible Errors',
|
|
12
|
-
recommended:
|
|
12
|
+
recommended: false,
|
|
13
13
|
manifest: true
|
|
14
14
|
},
|
|
15
15
|
defaultOptions: [{ target: 'mv3', allowedPermissions: [] }],
|