eslint-plugin-greasemonkey 1.8.4 → 1.9.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 +304 -64
- package/docs/LICENSE.md +1 -1
- package/package.json +47 -41
- package/src/data/headers.js +24 -0
- package/src/index.d.ts +33 -3
- package/src/index.js +5 -80
- package/src/rules/meta-order.js +3 -4
- package/src/rules/meta-spacing.js +2 -2
- package/src/rules/no-duplicate-keys.js +2 -2
- package/src/rules/no-insecure-fetch.js +109 -0
- package/src/rules/no-insecure-messaging.js +68 -0
- package/src/rules/no-invalid-grants.js +3 -3
- package/src/rules/no-invalid-headers.js +3 -3
- package/src/rules/no-missing-grants.js +3 -3
- package/src/rules/no-missing-resources.js +4 -3
- package/src/rules/no-unused-grants.js +2 -2
- package/src/rules/no-unused-resources.js +6 -5
- package/src/rules/prefer-gm-add-style.js +66 -0
- package/src/rules/prefer-match.js +4 -3
- package/src/rules/require-homepage-pair.js +3 -4
- package/src/rules/require-matching-localized-fields.js +2 -2
- package/src/rules/require-required-fields.js +2 -2
- package/src/rules/require-space-before-meta-key.js +2 -2
- package/src/rules/require-sri-hashing.js +3 -3
- package/src/rules/require-update-download-pair.js +3 -4
- package/src/rules/require-user-js-filename.js +2 -2
- package/src/rules/valid-field-values.js +2 -2
- package/src/utils/deprecations.js +76 -0
- package/src/utils/init.js +91 -0
- package/src/utils/metadata.js +31 -0
- package/src/utils/rules.js +7 -0
- package/src/utils/template.js +21 -0
- package/src/utils.js +0 -119
package/docs/LICENSE.md
CHANGED
package/package.json
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-greasemonkey",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "ESLint rules for Greasemonkey/Tampermonkey userscripts",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
|
|
5
|
+
"keywords": [
|
|
6
|
+
"eslint",
|
|
7
|
+
"eslint-plugin",
|
|
8
|
+
"eslintplugin",
|
|
9
|
+
"greasemonkey",
|
|
10
|
+
"metadata",
|
|
11
|
+
"security",
|
|
12
|
+
"tampermonkey",
|
|
13
|
+
"userscript"
|
|
14
|
+
],
|
|
14
15
|
"homepage": "https://codeberg.org/adamlui/eslint-plugin-greasemonkey/#readme",
|
|
15
16
|
"bugs": {
|
|
16
17
|
"url": "https://codeberg.org/adamlui/eslint-plugin-greasemonkey/issues",
|
|
17
18
|
"email": "support@adamlui.com"
|
|
18
19
|
},
|
|
19
|
-
"
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://codeberg.org/adamlui/eslint-plugin-greasemonkey.git"
|
|
23
|
+
},
|
|
20
24
|
"funding": [
|
|
21
25
|
{
|
|
22
26
|
"type": "ko-fi",
|
|
@@ -27,39 +31,34 @@
|
|
|
27
31
|
"url": "https://paypal.me/adamlui"
|
|
28
32
|
}
|
|
29
33
|
],
|
|
30
|
-
"
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"author": {
|
|
36
|
+
"name": "Adam Lui",
|
|
37
|
+
"email": "adam@kudoai.org",
|
|
38
|
+
"url": "https://codeberg.org/adamlui"
|
|
39
|
+
},
|
|
40
|
+
"type": "module",
|
|
41
|
+
"exports": {
|
|
42
|
+
".": {
|
|
43
|
+
"types": "./src/index.d.ts",
|
|
44
|
+
"default": "./src/index.js"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"main": "./src/index.js",
|
|
48
|
+
"types": "./src/index.d.ts",
|
|
31
49
|
"files": [
|
|
32
50
|
"docs/",
|
|
33
51
|
"src/"
|
|
34
52
|
],
|
|
35
|
-
"types": "src/index.d.ts",
|
|
36
53
|
"scripts": {
|
|
37
|
-
"lint": "eslint . --cache && yamllint .",
|
|
38
|
-
"lint:all": "eslint . && yamllint .",
|
|
39
|
-
"lint:fix": "eslint . --fix --cache && yamllint .",
|
|
40
|
-
"lint:fix-all": "eslint . --fix && yamllint .",
|
|
41
|
-
"test": "node tests/run.js",
|
|
42
|
-
"demo": "node demo/run.js",
|
|
43
|
-
"bump:patch": "bash utils/bump.sh patch",
|
|
44
|
-
"bump:minor": "bash utils/bump.sh minor",
|
|
45
54
|
"bump:feat": "npm run bump:minor",
|
|
46
|
-
"bump:major": "bash utils/bump.sh major"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"eslint",
|
|
54
|
-
"eslint-plugin",
|
|
55
|
-
"eslintplugin",
|
|
56
|
-
"greasemonkey",
|
|
57
|
-
"metadata",
|
|
58
|
-
"tampermonkey",
|
|
59
|
-
"userscript"
|
|
60
|
-
],
|
|
61
|
-
"peerDependencies": {
|
|
62
|
-
"eslint": ">=9.0.0"
|
|
55
|
+
"bump:major": "bash utils/bump.sh major",
|
|
56
|
+
"bump:minor": "bash utils/bump.sh minor",
|
|
57
|
+
"bump:patch": "bash utils/bump.sh patch",
|
|
58
|
+
"demo": "eslint demo",
|
|
59
|
+
"lint": "pre-commit run",
|
|
60
|
+
"lint:all": "pre-commit run --all-files",
|
|
61
|
+
"test": "node tests/run"
|
|
63
62
|
},
|
|
64
63
|
"devDependencies": {
|
|
65
64
|
"@e18e/eslint-plugin": "^0.8.1",
|
|
@@ -68,16 +67,23 @@
|
|
|
68
67
|
"@eslint/markdown": "^7.5.1",
|
|
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.3",
|
|
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",
|
|
75
74
|
"eslint-plugin-no-secrets": "^2.3.3",
|
|
76
75
|
"eslint-plugin-no-unsanitized": "^4.1.5",
|
|
76
|
+
"eslint-plugin-package-json": "^1.10.1",
|
|
77
77
|
"eslint-plugin-promise": "^7.3.0",
|
|
78
78
|
"eslint-plugin-regexp": "^3.3.1",
|
|
79
79
|
"eslint-plugin-security": "^4.2.0",
|
|
80
|
-
"
|
|
81
|
-
"
|
|
80
|
+
"eslint-plugin-woodpecker": "^1.0.1",
|
|
81
|
+
"globals": "^17.13.0"
|
|
82
|
+
},
|
|
83
|
+
"peerDependencies": {
|
|
84
|
+
"eslint": ">=9.0.0"
|
|
85
|
+
},
|
|
86
|
+
"engines": {
|
|
87
|
+
"node": ">=18.18.0"
|
|
82
88
|
}
|
|
83
89
|
}
|
package/src/data/headers.js
CHANGED
|
@@ -34,3 +34,27 @@ for (const manager of managers) {
|
|
|
34
34
|
.map(([header]) => header)
|
|
35
35
|
validHeaders.set(manager, new Set([...universalHeaders, ...specific]))
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
const urlHeaderTypes = {
|
|
39
|
+
connect: 'host-pattern',
|
|
40
|
+
contributionURL: 'url',
|
|
41
|
+
defaulticon: 'url',
|
|
42
|
+
downloadURL: 'url',
|
|
43
|
+
exclude: 'pattern',
|
|
44
|
+
'exclude-match': 'pattern',
|
|
45
|
+
homepage: 'url',
|
|
46
|
+
homepageURL: 'url',
|
|
47
|
+
icon: 'url',
|
|
48
|
+
icon64: 'url',
|
|
49
|
+
include: 'pattern',
|
|
50
|
+
match: 'pattern',
|
|
51
|
+
require: 'url',
|
|
52
|
+
'require-css': 'url',
|
|
53
|
+
resource: 'named-url',
|
|
54
|
+
source: 'url',
|
|
55
|
+
supportURL: 'url',
|
|
56
|
+
updateURL: 'url',
|
|
57
|
+
website: 'url'
|
|
58
|
+
}
|
|
59
|
+
const knownHeaders = new Set([...universalHeaders, ...managerHeaders.keys()])
|
|
60
|
+
export const urlHeaders = new Map(Object.entries(urlHeaderTypes).filter(([name]) => knownHeaders.has(name)))
|
package/src/index.d.ts
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
|
+
type RuleSeverity = 0 | 1 | 2 | 'off' | 'warn' | 'error'
|
|
2
|
+
type RuleSetting = RuleSeverity | [RuleSeverity, ...unknown[]]
|
|
3
|
+
|
|
4
|
+
type ConfigName =
|
|
5
|
+
| 'recommended'
|
|
6
|
+
| 'strict'
|
|
7
|
+
| 'problem'
|
|
8
|
+
| 'tampermonkey'
|
|
9
|
+
| 'violentmonkey'
|
|
10
|
+
| 'greasemonkey'
|
|
11
|
+
| 'scriptcat'
|
|
12
|
+
| 'adguard'
|
|
13
|
+
|
|
14
|
+
interface PluginConfig {
|
|
15
|
+
plugins: Record<string, unknown>
|
|
16
|
+
rules: Record<string, RuleSetting>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface PluginRule {
|
|
20
|
+
meta: {
|
|
21
|
+
type?: 'problem' | 'suggestion' | 'layout'
|
|
22
|
+
deprecated?: boolean | {
|
|
23
|
+
message?: string
|
|
24
|
+
replacedBy?: Array<{ rule: { name: string } }>
|
|
25
|
+
}
|
|
26
|
+
docs?: Record<string, unknown>
|
|
27
|
+
}
|
|
28
|
+
create: (context: unknown) => Record<string, (...args: unknown[]) => void>
|
|
29
|
+
}
|
|
30
|
+
|
|
1
31
|
declare const plugin: {
|
|
2
|
-
rules: Record<string,
|
|
3
|
-
configs:
|
|
32
|
+
rules: Record<string, PluginRule>
|
|
33
|
+
configs: Record<ConfigName, PluginConfig>
|
|
4
34
|
}
|
|
5
35
|
|
|
6
|
-
export default plugin
|
|
36
|
+
export { plugin as default, plugin as greasemonkey }
|
package/src/index.js
CHANGED
|
@@ -1,84 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import init from './utils/init.js'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const plugin = init.plugin()
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import noInvalidGrants from './rules/no-invalid-grants.js'
|
|
9
|
-
import noInvalidHeaders from './rules/no-invalid-headers.js'
|
|
10
|
-
import noMissingGrants from './rules/no-missing-grants.js'
|
|
11
|
-
import noMissingResources from './rules/no-missing-resources.js'
|
|
12
|
-
import noUnusedGrants from './rules/no-unused-grants.js'
|
|
13
|
-
import noUnusedResources from './rules/no-unused-resources.js'
|
|
14
|
-
import preferMatch from './rules/prefer-match.js'
|
|
15
|
-
import requireBlankLineAfterMetaBlock from './rules/require-blank-line-after-meta-block.js'
|
|
16
|
-
import requireHomepagePair from './rules/require-homepage-pair.js'
|
|
17
|
-
import requireMatchingLocalizedFields from './rules/require-matching-localized-fields.js'
|
|
18
|
-
import requireRequiredFields from './rules/require-required-fields.js'
|
|
19
|
-
import requireSpaceBeforeMetaKey from './rules/require-space-before-meta-key.js'
|
|
20
|
-
import requireSRIhashing from './rules/require-sri-hashing.js'
|
|
21
|
-
import requireUpdateDownloadPair from './rules/require-update-download-pair.js'
|
|
22
|
-
import requireUserJsFilename from './rules/require-user-js-filename.js'
|
|
23
|
-
import validFieldValues from './rules/valid-field-values.js'
|
|
24
|
-
|
|
25
|
-
import { bundleRules, initDeprecatedAlias } from './utils.js'
|
|
26
|
-
|
|
27
|
-
const plugin = {
|
|
28
|
-
rules: {
|
|
29
|
-
'meta-order': metaOrder,
|
|
30
|
-
'meta-spacing': metaSpacing,
|
|
31
|
-
'no-duplicate-keys': noDuplicateKeys,
|
|
32
|
-
'no-invalid-grants': noInvalidGrants,
|
|
33
|
-
'no-invalid-headers': noInvalidHeaders,
|
|
34
|
-
'no-missing-grants': noMissingGrants,
|
|
35
|
-
'no-unused-grants': noUnusedGrants,
|
|
36
|
-
'no-missing-resources': noMissingResources,
|
|
37
|
-
'no-unused-resources': noUnusedResources,
|
|
38
|
-
'prefer-match': preferMatch,
|
|
39
|
-
'require-blank-line-after-meta-block': requireBlankLineAfterMetaBlock,
|
|
40
|
-
'require-homepage-pair': requireHomepagePair,
|
|
41
|
-
'require-matching-localized-fields': requireMatchingLocalizedFields,
|
|
42
|
-
'require-required-fields': requireRequiredFields,
|
|
43
|
-
'require-space-before-meta-key': requireSpaceBeforeMetaKey,
|
|
44
|
-
'require-sri-hashing': requireSRIhashing,
|
|
45
|
-
'require-update-download-pair': requireUpdateDownloadPair,
|
|
46
|
-
'require-user-js-filename': requireUserJsFilename,
|
|
47
|
-
'valid-field-values': validFieldValues
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
for (const [ruleId, rule] of Object.entries(plugin.rules))
|
|
52
|
-
rule.meta.docs.url = `https://codeberg.org/adamlui/eslint-plugin-greasemonkey/#${ruleId}`
|
|
53
|
-
|
|
54
|
-
for (const entry of Object.entries(oldRules))
|
|
55
|
-
initDeprecatedAlias(plugin, entry)
|
|
56
|
-
|
|
57
|
-
plugin.configs = {
|
|
58
|
-
recommended: {
|
|
59
|
-
plugins: { greasemonkey: plugin },
|
|
60
|
-
rules: bundleRules(plugin.rules, rule => rule.meta.docs.recommended, 'greasemonkey')
|
|
61
|
-
},
|
|
62
|
-
strict: {
|
|
63
|
-
plugins: { greasemonkey: plugin },
|
|
64
|
-
rules: bundleRules(plugin.rules, () => true, 'greasemonkey')
|
|
65
|
-
},
|
|
66
|
-
problem: {
|
|
67
|
-
plugins: { greasemonkey: plugin },
|
|
68
|
-
rules: bundleRules(plugin.rules, rule => rule.meta.type == 'problem', 'greasemonkey')
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
for (const manager of ['tampermonkey', 'violentmonkey', 'greasemonkey', 'scriptcat', 'adguard'])
|
|
73
|
-
plugin.configs[manager] = {
|
|
74
|
-
plugins: { greasemonkey: plugin },
|
|
75
|
-
rules: {
|
|
76
|
-
...plugin.configs.recommended.rules,
|
|
77
|
-
'greasemonkey/no-invalid-grants': ['error', { target: manager }],
|
|
78
|
-
'greasemonkey/no-invalid-headers': ['error', { target: manager }],
|
|
79
|
-
...(['tampermonkey', 'scriptcat', 'adguard'].includes(manager) &&
|
|
80
|
-
{ 'greasemonkey/require-update-download-pair': 'error' })
|
|
81
|
-
}
|
|
82
|
-
}
|
|
5
|
+
init.rules(plugin)
|
|
6
|
+
init.deprecations(plugin)
|
|
7
|
+
init.configs(plugin)
|
|
83
8
|
|
|
84
9
|
export { plugin as default, plugin as greasemonkey }
|
package/src/rules/meta-order.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
3
|
const defaultOrder = [
|
|
4
4
|
'name', 'description', 'author', 'namespace', 'version', 'license', 'icon', 'icon64',
|
|
@@ -30,8 +30,7 @@ export default {
|
|
|
30
30
|
type: 'layout',
|
|
31
31
|
docs: {
|
|
32
32
|
description: 'Enforce an order for metadata keys',
|
|
33
|
-
category: 'Stylistic Issues'
|
|
34
|
-
recommended: false
|
|
33
|
+
category: 'Stylistic Issues'
|
|
35
34
|
},
|
|
36
35
|
fixable: 'code',
|
|
37
36
|
schema: [{
|
|
@@ -54,7 +53,7 @@ export default {
|
|
|
54
53
|
return {
|
|
55
54
|
Program() {
|
|
56
55
|
const sourceCode = context.sourceCode,
|
|
57
|
-
entries =
|
|
56
|
+
entries = metadata.extract(sourceCode),
|
|
58
57
|
violation = findViolation(entries, master) ?? (custom.length && findViolation(entries, custom))
|
|
59
58
|
if (!violation) return
|
|
60
59
|
const [first, second] = violation
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
3
|
const re = { leadingWhitespace: /^\s*/, whitespace: /\s+/ }
|
|
4
4
|
|
|
@@ -30,7 +30,7 @@ export default {
|
|
|
30
30
|
return {
|
|
31
31
|
Program() {
|
|
32
32
|
const sourceCode = context.sourceCode,
|
|
33
|
-
metaEntries =
|
|
33
|
+
metaEntries = metadata.extract(sourceCode)
|
|
34
34
|
if (!metaEntries.length) return
|
|
35
35
|
let targetCol = metaEntries.reduce((max, entry) => Math.max(max, entry.key.length), 0) + minSpaces +4
|
|
36
36
|
for (const entry of metaEntries)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
3
|
const multiValKeys = new Set([
|
|
4
4
|
'compatible', 'connect', 'description', 'exclude', 'exclude-match', 'grant',
|
|
@@ -20,7 +20,7 @@ export default {
|
|
|
20
20
|
create(context) {
|
|
21
21
|
return {
|
|
22
22
|
Program() {
|
|
23
|
-
const metaEntries =
|
|
23
|
+
const metaEntries = metadata.extract(context.sourceCode),
|
|
24
24
|
seenEntries = new Set()
|
|
25
25
|
for (const entry of metaEntries) {
|
|
26
26
|
const baseKey = entry.key.split(':')[0]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { urlHeaders } from '../data/headers.js'
|
|
2
|
+
|
|
3
|
+
const re = {
|
|
4
|
+
insecureURL: /^http:\/\/(?!localhost\b|127\.0\.0\.1\b|\[::1\])/i,
|
|
5
|
+
meta: {
|
|
6
|
+
end: /^\/\/\s*==\/UserScript==\s*$/,
|
|
7
|
+
line: /^\/\/\s*@([\w-]+)(?::[\w-]+)?\s+(\S.*)$/, // eslint-disable-line security/detect-unsafe-regex
|
|
8
|
+
start: /^\/\/\s*==UserScript==\s*$/
|
|
9
|
+
},
|
|
10
|
+
newline: /\r?\n/,
|
|
11
|
+
whitespace: /\s+/
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
meta: {
|
|
16
|
+
type: 'problem',
|
|
17
|
+
docs: {
|
|
18
|
+
description: 'Flags `fetch()`, `XMLHttpRequest.open()`, `GM_xmlhttpRequest()`, `GM.xmlHttpRequest()` calls and URL fields that use `http://`',
|
|
19
|
+
category: 'Possible Errors',
|
|
20
|
+
recommended: true,
|
|
21
|
+
security: true
|
|
22
|
+
},
|
|
23
|
+
schema: [],
|
|
24
|
+
messages: {
|
|
25
|
+
insecureFetch: '`fetch()` to "{{url}}" insecure. Use HTTPS or a relative path instead.',
|
|
26
|
+
insecureXHR: '`XMLHttpRequest.open()` to "{{url}}" insecure. Use HTTPS or a relative path instead.',
|
|
27
|
+
insecureGMXHR: '`{{callee}}()` to "{{url}}" insecure. Use HTTPS or a relative path instead.',
|
|
28
|
+
insecureMetadata: '`@{{key}}` URL "{{url}}" insecure. Use HTTPS instead.'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
create(context) {
|
|
33
|
+
const xhrVars = new Set(),
|
|
34
|
+
sourceCode = context.sourceCode ?? context.getSourceCode()
|
|
35
|
+
function isInsecure(url) { return typeof url == 'string' && re.insecureURL.test(url) }
|
|
36
|
+
function reportMetadata() {
|
|
37
|
+
const lines = sourceCode.text.split(re.newline)
|
|
38
|
+
let inBlock = false
|
|
39
|
+
for (const [i, rawLine] of lines.entries()) {
|
|
40
|
+
const line = rawLine.trimEnd()
|
|
41
|
+
if (re.meta.start.test(line.trim())) { inBlock = true ; continue }
|
|
42
|
+
if (inBlock && re.meta.end.test(line.trim())) break
|
|
43
|
+
if (!inBlock) continue
|
|
44
|
+
const match = line.match(re.meta.line) ; if (!match) continue
|
|
45
|
+
const key = match[1]
|
|
46
|
+
const type = urlHeaders.get(key) ; if (!type) continue
|
|
47
|
+
const tokens = match[2].split(re.whitespace),
|
|
48
|
+
url = type == 'named-url' ? tokens[1] : tokens[0]
|
|
49
|
+
if (!url || !isInsecure(url)) continue
|
|
50
|
+
const col = line.lastIndexOf(url)
|
|
51
|
+
context.report({
|
|
52
|
+
loc: { start: { line: i +1, column: col },
|
|
53
|
+
end: { line: i +1, column: col + url.length }},
|
|
54
|
+
messageId: 'insecureMetadata', data: { key, url }
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function reportGMXHR(node) {
|
|
59
|
+
const url = urlFromLiteral(node.arguments[0]) ?? urlFromOptions(node.arguments[0]) ; if (!url) return
|
|
60
|
+
const callee = node.callee.type == 'MemberExpression'
|
|
61
|
+
? `${node.callee.object.name}.${node.callee.property.name}`
|
|
62
|
+
: node.callee.name
|
|
63
|
+
context.report({ node: node.arguments[0], messageId: 'insecureGMXHR', data: { callee, url }})
|
|
64
|
+
}
|
|
65
|
+
function urlFromLiteral(node) {
|
|
66
|
+
return (node?.type == 'Literal'
|
|
67
|
+
&& typeof node.value == 'string'
|
|
68
|
+
&& isInsecure(node.value)
|
|
69
|
+
) ? node.value : null
|
|
70
|
+
}
|
|
71
|
+
function urlFromOptions(node) {
|
|
72
|
+
if (node?.type != 'ObjectExpression') return null
|
|
73
|
+
for (const prop of node.properties)
|
|
74
|
+
if (prop.type == 'Property' && (prop.key?.name == 'url' || prop.key?.value == 'url')) {
|
|
75
|
+
const val = prop.value
|
|
76
|
+
if (val?.type == 'Literal' && isInsecure(val.value)) return val.value
|
|
77
|
+
}
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
Program() { reportMetadata() },
|
|
83
|
+
VariableDeclarator(node) {
|
|
84
|
+
if (node.init?.type == 'NewExpression'
|
|
85
|
+
&& node.init.callee?.name == 'XMLHttpRequest'
|
|
86
|
+
&& node.id.type == 'Identifier'
|
|
87
|
+
) xhrVars.add(node.id.name)
|
|
88
|
+
},
|
|
89
|
+
CallExpression(node) {
|
|
90
|
+
if (node.callee.type == 'Identifier' && node.callee.name == 'fetch') {
|
|
91
|
+
const url = urlFromLiteral(node.arguments[0])
|
|
92
|
+
if (url) context.report({ node: node.arguments[0], messageId: 'insecureFetch', data: { url }})
|
|
93
|
+
} else if (node.callee.type == 'MemberExpression'
|
|
94
|
+
&& node.callee.property?.name == 'open'
|
|
95
|
+
&& node.callee.object?.type == 'Identifier'
|
|
96
|
+
&& xhrVars.has(node.callee.object.name)
|
|
97
|
+
) { // xhr.open('GET', 'http://…')
|
|
98
|
+
const url = urlFromLiteral(node.arguments[1])
|
|
99
|
+
if (url) context.report({ node: node.arguments[1], messageId: 'insecureXHR', data: { url }})
|
|
100
|
+
} else if (node.callee.type == 'Identifier'
|
|
101
|
+
&& node.callee.name == 'GM_xmlhttpRequest'
|
|
102
|
+
|| node.callee.type == 'MemberExpression'
|
|
103
|
+
&& node.callee.object?.name == 'GM'
|
|
104
|
+
&& node.callee.property?.name == 'xmlHttpRequest'
|
|
105
|
+
) reportGMXHR(node)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const re = {
|
|
2
|
+
messageTargets: new Set(['window', 'self', 'unsafeWindow']),
|
|
3
|
+
postmsgValidation: /\b\w+\.(?:origin|source)\b/
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'problem',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Requires origin/source validation + explicit `postMessage()` target',
|
|
11
|
+
category: 'Possible Errors',
|
|
12
|
+
recommended: true,
|
|
13
|
+
security: true
|
|
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
|
+
}],
|
|
26
|
+
messages: {
|
|
27
|
+
unvalidatedPostMessage: '`{{target}}` message handler must validate `event.origin`/`event.source` '
|
|
28
|
+
+ 'before processing messages.',
|
|
29
|
+
wildcardTarget: '`postMessage` to `"*"` leaks data to any origin. '
|
|
30
|
+
+ 'Explicitly specify `targetOrigin` instead.'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
create(context) {
|
|
35
|
+
const opts = context.options[0] || {},
|
|
36
|
+
disallowWildcard = opts.disallowWildcardTarget !== false,
|
|
37
|
+
sourceCode = context.sourceCode ?? context.getSourceCode()
|
|
38
|
+
function checkPostMessageHandler(node, handlerNode, target) {
|
|
39
|
+
if (!handlerNode) return
|
|
40
|
+
const body = sourceCode.getText(handlerNode)
|
|
41
|
+
if (re.postmsgValidation.test(body)) return
|
|
42
|
+
context.report({ node, messageId: 'unvalidatedPostMessage', data: { target }})
|
|
43
|
+
}
|
|
44
|
+
function isMessageTarget(obj) {
|
|
45
|
+
return obj?.type == 'Identifier' && re.messageTargets.has(obj.name)
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
CallExpression(node) {
|
|
49
|
+
if (node.callee.type != 'MemberExpression') return
|
|
50
|
+
if (node.callee.property?.name == 'addEventListener'
|
|
51
|
+
&& node.arguments[0]?.value == 'message'
|
|
52
|
+
&& isMessageTarget(node.callee.object)
|
|
53
|
+
) checkPostMessageHandler(node, node.arguments[1], node.callee.object.name)
|
|
54
|
+
if (disallowWildcard && node.callee.property?.name == 'postMessage') {
|
|
55
|
+
const target = node.arguments[1]
|
|
56
|
+
if (target?.type == 'Literal' && target.value == '*')
|
|
57
|
+
context.report({ node: target, messageId: 'wildcardTarget' })
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
AssignmentExpression(node) {
|
|
61
|
+
if (node.left.type == 'MemberExpression'
|
|
62
|
+
&& node.left.property?.name == 'onmessage'
|
|
63
|
+
&& isMessageTarget(node.left.object)
|
|
64
|
+
) checkPostMessageHandler(node, node.right, node.left.object.name)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { allAPIs, supportsAPI } from '../data/apis.js'
|
|
4
4
|
|
|
5
5
|
const managers = ['tampermonkey', 'violentmonkey', 'greasemonkey', 'scriptcat', 'adguard'],
|
|
6
6
|
specialGrants = new Set(['none', 'unsafeWindow', 'window.close', 'window.focus', 'window.onurlchange']),
|
|
@@ -51,7 +51,7 @@ export default {
|
|
|
51
51
|
allowedGrants = new Set(Array.isArray(rawAllowed) ? rawAllowed : [rawAllowed])
|
|
52
52
|
return {
|
|
53
53
|
Program() {
|
|
54
|
-
const metaEntries =
|
|
54
|
+
const metaEntries = metadata.extract(context.sourceCode),
|
|
55
55
|
grantEntries = metaEntries.filter(entry => entry.key == 'grant')
|
|
56
56
|
for (const grant of grantEntries) {
|
|
57
57
|
const grantName = grant.value.trim()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { managerHeaders, managers, localizedHeaders, validHeaders } from '../data/headers.js'
|
|
4
4
|
|
|
5
5
|
function parseAllowedPattern(entry) {
|
|
6
6
|
if (typeof entry != 'string' || !entry.startsWith('/')) return null
|
|
@@ -62,7 +62,7 @@ export default {
|
|
|
62
62
|
}
|
|
63
63
|
return {
|
|
64
64
|
Program() {
|
|
65
|
-
const metaEntries =
|
|
65
|
+
const metaEntries = metadata.extract(context.sourceCode)
|
|
66
66
|
for (const entry of metaEntries) {
|
|
67
67
|
if (allowedExact.has(entry.key)) continue
|
|
68
68
|
if (allowedRegexes.some(re => re.test(entry.key))) continue
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { gmLegacyAPIs, gmModernAPIs, scAPIs } from '../data/apis.js'
|
|
4
4
|
|
|
5
5
|
const apis = new Set([...gmLegacyAPIs, ...gmModernAPIs, ...scAPIs]),
|
|
6
6
|
grantlessAPIs = new Set(['GM_info', 'GM.info']) // always available, no grant required
|
|
@@ -37,7 +37,7 @@ export default {
|
|
|
37
37
|
'Program:exit'() {
|
|
38
38
|
if (!usedAPIs.size) return
|
|
39
39
|
const declared = new Set(
|
|
40
|
-
|
|
40
|
+
metadata.extract(context.sourceCode)
|
|
41
41
|
.filter(entry => entry.key == 'grant')
|
|
42
42
|
.map(entry => entry.value.trim())
|
|
43
43
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
|
+
import template from '../utils/template.js'
|
|
2
3
|
|
|
3
4
|
const resourceGetterNames = new Set(['GM_getResourceText', 'GM_getResourceURL']),
|
|
4
5
|
re = { specialChars: /[.*+?^${}()|[\]\\]/g, whitespace: /\s+/ }
|
|
@@ -24,7 +25,7 @@ export default {
|
|
|
24
25
|
if (arg?.type == 'Literal' && typeof arg.value == 'string')
|
|
25
26
|
usedRefs.push({ names: [arg.value], pattern: null, node })
|
|
26
27
|
else if (arg?.type == 'TemplateLiteral') {
|
|
27
|
-
const resolved =
|
|
28
|
+
const resolved = template.resolve(arg)
|
|
28
29
|
if (resolved) usedRefs.push({ names: resolved, pattern: null, node })
|
|
29
30
|
else usedRefs.push({
|
|
30
31
|
names: null,
|
|
@@ -42,7 +43,7 @@ export default {
|
|
|
42
43
|
'Program:exit'() {
|
|
43
44
|
if (!usedRefs.length) return
|
|
44
45
|
const declared = new Set(
|
|
45
|
-
|
|
46
|
+
metadata.extract(context.sourceCode)
|
|
46
47
|
.filter(entry => entry.key == 'resource')
|
|
47
48
|
.map(entry => entry.value.trim().split(re.whitespace)[0])
|
|
48
49
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
meta: {
|
|
@@ -30,7 +30,7 @@ export default {
|
|
|
30
30
|
},
|
|
31
31
|
'Program:exit'() {
|
|
32
32
|
const sourceCode = context.sourceCode,
|
|
33
|
-
metaEntries =
|
|
33
|
+
metaEntries = metadata.extract(sourceCode),
|
|
34
34
|
grants = metaEntries.filter(entry => entry.key == 'grant')
|
|
35
35
|
if (!grants.length) return
|
|
36
36
|
for (const grant of grants) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
|
+
import template from '../utils/template.js'
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
|
|
4
|
+
const re = { specialChars: /[.*+?^${}()|[\]\\]/g, whitespace: /\s+/ },
|
|
5
|
+
resourceGetterNames = new Set(['GM_getResourceText', 'GM_getResourceURL'])
|
|
5
6
|
|
|
6
7
|
export default {
|
|
7
8
|
meta: {
|
|
@@ -25,7 +26,7 @@ export default {
|
|
|
25
26
|
if (arg?.type == 'Literal' && typeof arg.value == 'string')
|
|
26
27
|
usedResources.add(arg.value)
|
|
27
28
|
else if (arg?.type == 'TemplateLiteral') {
|
|
28
|
-
const resolved =
|
|
29
|
+
const resolved = template.resolve(arg)
|
|
29
30
|
if (resolved)
|
|
30
31
|
for (const name of resolved) usedResources.add(name)
|
|
31
32
|
else usedPatterns.push(new RegExp( // eslint-disable-line security/detect-non-literal-regexp
|
|
@@ -33,7 +34,7 @@ export default {
|
|
|
33
34
|
}
|
|
34
35
|
},
|
|
35
36
|
'Program:exit'() {
|
|
36
|
-
const metaEntries =
|
|
37
|
+
const metaEntries = metadata.extract(context.sourceCode),
|
|
37
38
|
resources = metaEntries.filter(entry => entry.key == 'resource')
|
|
38
39
|
if (!resources.length) return
|
|
39
40
|
for (const res of resources) {
|