eslint-plugin-chrome-extensions 2.7.0 → 2.8.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 +266 -43
- package/package.json +9 -8
- package/src/data/apis.js +74 -88
- package/src/data/manifest.js +3 -1
- package/src/data/regex.js +2 -1
- package/src/index.d.ts +42 -0
- package/src/rules/css-spacing.js +29 -21
- package/src/rules/manifest-key-order.js +1 -1
- package/src/rules/manifest-spacing.js +1 -1
- package/src/rules/no-content-style-leak.js +12 -11
- package/src/rules/no-deprecated-apis.js +2 -2
- package/src/rules/no-host-page-selectors.js +5 -6
- package/src/rules/no-inline-event-handlers.js +116 -0
- package/src/rules/no-inline-scripts.js +65 -0
- package/src/rules/no-insecure-fetch.js +7 -6
- package/src/rules/no-insecure-messaging.js +4 -4
- package/src/rules/no-invalid-locales.js +3 -5
- package/src/rules/no-missing-default-locale.js +1 -1
- package/src/rules/no-missing-permissions.js +2 -3
- package/src/rules/no-missing-web-accessible-resources.js +2 -2
- package/src/rules/no-remote-code.js +77 -28
- package/src/rules/no-remote-objects.js +55 -0
- package/src/rules/no-remote-urls.js +20 -20
- package/src/rules/no-scrollbar-styling.js +8 -10
- package/src/rules/no-unrestricted-host-permissions.js +1 -1
- package/src/rules/no-unused-permissions.js +5 -4
- package/src/rules/require-license-file.js +1 -1
- package/src/rules/require-mv3.js +3 -2
- package/src/rules/valid-filenames.js +3 -3
- package/src/rules/valid-manifest-icons.js +2 -2
- package/src/rules/valid-mv3-schema.js +5 -5
- package/src/rules/valid-url-fields.js +2 -2
- package/src/rules/valid-version-field.js +2 -2
- package/src/utils/api.js +20 -0
- package/src/utils/extension.js +1 -3
- package/src/utils/html.js +29 -0
- package/src/utils/i18n.js +2 -2
- package/src/utils/init.js +11 -3
- package/src/utils/license.js +1 -1
- package/src/utils/node.js +3 -1
- package/src/utils/pkg.js +19 -2
- package/src/utils/rules.js +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import array from '../utils/array.js'
|
|
2
|
+
import html from '../utils/html.js'
|
|
3
|
+
|
|
4
|
+
const re = {
|
|
5
|
+
nonExecutable: /^(?:application\/(?:ld\+)?json|importmap|speculationrules|text\/(?:x-)?template)$/i,
|
|
6
|
+
script: /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Disallow inline `<script>` bodies and remote `<script src>` in extension HTML',
|
|
14
|
+
category: 'Possible Errors',
|
|
15
|
+
html: true
|
|
16
|
+
},
|
|
17
|
+
defaultOptions: [{ allowedSrcPrefixes: ['chrome-extension://'] }],
|
|
18
|
+
schema: [{
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
allowedSrcPrefixes: {
|
|
22
|
+
oneOf: [
|
|
23
|
+
{ type: 'string', description: 'A single allowed src prefix' },
|
|
24
|
+
{ type: 'array', items: { type: 'string' }, description: 'List of allowed src prefixes' }
|
|
25
|
+
],
|
|
26
|
+
description: 'Additional src prefixes to permit'
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
additionalProperties: false
|
|
30
|
+
}],
|
|
31
|
+
messages: {
|
|
32
|
+
inlineBody: 'Inline `<script>` bodies are blocked by MV3\'s `script-src \'self\'`. Move the code to an external file.',
|
|
33
|
+
remoteSrc: 'Remote `<script src="{{src}}">` is blocked by MV3\'s `script-src \'self\'`. Bundle the script or use `chrome.runtime.getURL()`.'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
create(context) {
|
|
38
|
+
const opts = context.options[0] || {},
|
|
39
|
+
allowedPrefixes = array.to(opts.allowedSrcPrefixes ?? ['chrome-extension://']),
|
|
40
|
+
text = context.sourceCode.text
|
|
41
|
+
let scanned = false
|
|
42
|
+
function scan() {
|
|
43
|
+
if (scanned) return
|
|
44
|
+
scanned = true
|
|
45
|
+
if (!html.isHTML(text)) return
|
|
46
|
+
for (const match of text.matchAll(re.script)) {
|
|
47
|
+
const [, attrs, body] = match,
|
|
48
|
+
type = html.pickAttrVal(attrs.match(html.re.attr.type))
|
|
49
|
+
if (type && re.nonExecutable.test(type.trim())) continue
|
|
50
|
+
const src = html.pickAttrVal(attrs.match(html.re.attr.src))
|
|
51
|
+
if (src !== null) {
|
|
52
|
+
const trimmed = src.trim()
|
|
53
|
+
if (trimmed && html.isRemote(trimmed, allowedPrefixes))
|
|
54
|
+
context.report({ loc: html.offsetToLoc(text, match.index),
|
|
55
|
+
messageId: 'remoteSrc', data: { src: trimmed }})
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
if (body && body.trim().length > 0)
|
|
59
|
+
context.report({ loc: html.offsetToLoc(text, match.index),
|
|
60
|
+
messageId: 'inlineBody' })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { 'Program:exit': scan, 'Document:exit': scan, '*': scan }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
const re = { insecureURL: /^http:\/\/(?!localhost\b|127\.0\.0\.1\b|\[::1\])/i }
|
|
2
2
|
|
|
3
|
+
function urlFromLiteral(node) {
|
|
4
|
+
return ( node?.type == 'Literal'
|
|
5
|
+
&& typeof node.value == 'string'
|
|
6
|
+
&& re.insecureURL.test(node.value)
|
|
7
|
+
) ? node.value : null
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
export default {
|
|
4
11
|
meta: {
|
|
5
12
|
type: 'problem',
|
|
@@ -18,12 +25,6 @@ export default {
|
|
|
18
25
|
|
|
19
26
|
create(context) {
|
|
20
27
|
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
28
|
return {
|
|
28
29
|
VariableDeclarator(node) {
|
|
29
30
|
if (node.init?.type == 'NewExpression'
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import api from '../utils/api.js'
|
|
1
2
|
import nodeUtils from '../utils/node.js'
|
|
2
|
-
import apis from '../data/apis.js'
|
|
3
3
|
|
|
4
|
-
const re = { postmsgValidation: /\b\w+\.(?:origin|source)\b/, senderValidation: /sender\.(?:id|origin)/ }
|
|
4
|
+
const re = { postmsgValidation: /\b\w+\.(?:origin|source)\b/, senderValidation: /sender\.(?:id|origin)/ },
|
|
5
|
+
isMessageTarget = obj => obj?.type == 'Identifier' && ['self', 'window'].includes(obj.name)
|
|
5
6
|
|
|
6
7
|
export default {
|
|
7
8
|
meta: {
|
|
@@ -40,13 +41,12 @@ export default {
|
|
|
40
41
|
if (re.postmsgValidation.test(body)) return
|
|
41
42
|
context.report({ node, messageId: 'unvalidatedPostMessage', data: { target }})
|
|
42
43
|
}
|
|
43
|
-
function isMessageTarget(obj) { return obj?.type == 'Identifier' && ['self', 'window'].includes(obj.name) }
|
|
44
44
|
return {
|
|
45
45
|
CallExpression(node) {
|
|
46
46
|
if (node.callee.type != 'MemberExpression') return
|
|
47
47
|
const path = nodeUtils.getMemberPath(node.callee) ; if (!path) return
|
|
48
48
|
const listenerRoot = path.endsWith('.addListener') ? path.slice(0, -'.addListener'.length) : null
|
|
49
|
-
if (listenerRoot &&
|
|
49
|
+
if (listenerRoot && api.isExternalMsgListener(listenerRoot)) {
|
|
50
50
|
const handler = node.arguments[0] ; if (!handler) return
|
|
51
51
|
const body = context.sourceCode.getText(handler)
|
|
52
52
|
if (!re.senderValidation.test(body))
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* eslint-disable security/detect-non-literal-fs-filename -- lint-time project inspection only */
|
|
2
|
-
|
|
3
1
|
import extension from '../utils/extension.js'
|
|
4
2
|
import fs from 'node:fs'
|
|
5
3
|
import i18nUtils from '../utils/i18n.js'
|
|
@@ -51,11 +49,11 @@ export default {
|
|
|
51
49
|
root = extension.findRoot(context)
|
|
52
50
|
if (root) {
|
|
53
51
|
const localesDir = path.join(root, '_locales')
|
|
54
|
-
try {
|
|
52
|
+
try { // oxlint-disable-next-line security/detect-non-literal-fs-filename
|
|
55
53
|
for (const entry of fs.readdirSync(localesDir, { withFileTypes: true }))
|
|
56
54
|
if (entry.isDirectory() && isInvalid(entry.name)) invalidFolders.push(entry.name)
|
|
57
|
-
invalidFolders.sort()
|
|
58
|
-
} catch {}
|
|
55
|
+
invalidFolders.sort((a, b) => a < b ? -1 : a > b ? 1 : 0)
|
|
56
|
+
} catch {}
|
|
59
57
|
}
|
|
60
58
|
const loc = { line: 1, column: 1 }
|
|
61
59
|
if (!isManifest) {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import apiUtils from '../utils/api.js'
|
|
1
2
|
import extension from '../utils/extension.js'
|
|
2
3
|
import nodeUtils from '../utils/node.js'
|
|
3
4
|
|
|
4
|
-
import apis from '../data/apis.js'
|
|
5
|
-
|
|
6
5
|
function queriesActiveTab(node) {
|
|
7
6
|
const arg = node.arguments[0] ; if (arg?.type != 'ObjectExpression') return false
|
|
8
7
|
return arg.properties.some(prop => {
|
|
@@ -34,7 +33,7 @@ export default {
|
|
|
34
33
|
CallExpression(node) {
|
|
35
34
|
if (node.callee.type != 'MemberExpression') return
|
|
36
35
|
const api = nodeUtils.getMemberPath(node.callee) ; if (!api) return
|
|
37
|
-
const permission =
|
|
36
|
+
const permission = apiUtils.findPermission(api) ; if (!permission || declared.has(permission)) return
|
|
38
37
|
const isActiveTabQuery = permission == 'tabs' && queriesActiveTab(node)
|
|
39
38
|
if (isActiveTabQuery && declared.has('activeTab')) return
|
|
40
39
|
const messageId = `missing${ isActiveTabQuery ? 'ActiveTab' : 'Permission' }`
|
|
@@ -16,8 +16,8 @@ function collectWARpatterns(manifest) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function patternToRegex(pattern) {
|
|
19
|
-
const escaped = pattern.
|
|
20
|
-
return new RegExp(`^${escaped}$`) //
|
|
19
|
+
const escaped = pattern.replaceAll(re.regexSpecial, '\\$&').replaceAll('*', '.*')
|
|
20
|
+
return new RegExp(`^${escaped}$`) // oxlint-disable-line security/detect-non-literal-regexp
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export default {
|
|
@@ -3,6 +3,38 @@ import nodeUtils from '../utils/node.js'
|
|
|
3
3
|
import manifest from '../data/manifest.js'
|
|
4
4
|
import regex from '../data/regex.js'
|
|
5
5
|
|
|
6
|
+
const re = { inlineHandler: /^on[a-z]+$/i, inlineHandlerInHTML: /\son[a-z]+\s*=/i }
|
|
7
|
+
|
|
8
|
+
function isCreateScript(node) {
|
|
9
|
+
return node?.type == 'CallExpression'
|
|
10
|
+
&& node.callee?.property?.name == 'createElement'
|
|
11
|
+
&& node.arguments[0]?.value == 'script'
|
|
12
|
+
}
|
|
13
|
+
function isInlineHandlerInString(node) {
|
|
14
|
+
return node?.type == 'Literal'
|
|
15
|
+
&& typeof node.value == 'string'
|
|
16
|
+
&& re.inlineHandlerInHTML.test(node.value)
|
|
17
|
+
}
|
|
18
|
+
function isLiteralURL(node) {
|
|
19
|
+
return node?.type == 'Literal'
|
|
20
|
+
&& typeof node.value == 'string'
|
|
21
|
+
&& regex.url.test(node.value)
|
|
22
|
+
}
|
|
23
|
+
function isObjectAssign(node) {
|
|
24
|
+
return node?.type == 'CallExpression'
|
|
25
|
+
&& node.callee?.type == 'MemberExpression'
|
|
26
|
+
&& node.callee.object?.name == 'Object'
|
|
27
|
+
&& node.callee.property?.name == 'assign'
|
|
28
|
+
}
|
|
29
|
+
function isScriptSrcProperty(node) {
|
|
30
|
+
const obj = node.parent
|
|
31
|
+
if (obj?.type != 'ObjectExpression') return false
|
|
32
|
+
const parent = obj.parent
|
|
33
|
+
if (isCreateScript(parent)) return true
|
|
34
|
+
if (isObjectAssign(parent) && isCreateScript(parent.arguments[0])) return true
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
|
|
6
38
|
export default {
|
|
7
39
|
meta: {
|
|
8
40
|
type: 'problem',
|
|
@@ -13,8 +45,7 @@ export default {
|
|
|
13
45
|
security: true
|
|
14
46
|
},
|
|
15
47
|
defaultOptions: [{
|
|
16
|
-
patterns:
|
|
17
|
-
'dynamic-import', 'remote-script-src', 'execute-script-url'],
|
|
48
|
+
patterns: manifest.patterns.remoteCode,
|
|
18
49
|
checkExecuteScriptClosures: true
|
|
19
50
|
}],
|
|
20
51
|
schema: [{
|
|
@@ -39,31 +70,17 @@ export default {
|
|
|
39
70
|
dynamicImport: 'Remote dynamic `import()` is forbidden under MV3\'s CSP. Use a relative path instead.',
|
|
40
71
|
remoteScriptSrc: 'Injecting remote `<script src>` is forbidden under MV3\'s CSP. Use `chrome.runtime.getURL()` for bundled scripts instead.',
|
|
41
72
|
executeScriptURL: '`chrome.scripting.executeScript` must use local `files`, not a remote URL.',
|
|
42
|
-
executeScriptClosure: '`chrome.scripting.executeScript({ func })` captures outer-scope variables that fail after serialization. Pass them via `args` instead.'
|
|
73
|
+
executeScriptClosure: '`chrome.scripting.executeScript({ func })` captures outer-scope variables that fail after serialization. Pass them via `args` instead.',
|
|
74
|
+
inlineEventHandler: 'Inline event handler `{{attr}}` is blocked by MV3\'s `script-src \'self\'`. Use `addEventListener()` instead.',
|
|
75
|
+
inlineScriptBody: 'Inline `<script>` body injection is blocked by MV3\'s `script-src \'self\'`. Move the code to an external file.'
|
|
43
76
|
}
|
|
44
77
|
},
|
|
45
78
|
|
|
46
79
|
create(context) {
|
|
47
80
|
const opts = context.options[0] || {},
|
|
48
|
-
patterns = new Set(opts.patterns || manifest.
|
|
81
|
+
patterns = new Set(opts.patterns || manifest.patterns.remoteCode),
|
|
49
82
|
checkClosures = opts.checkExecuteScriptClosures !== false,
|
|
50
83
|
scriptElems = new Set()
|
|
51
|
-
function isCreateScript(node) {
|
|
52
|
-
return node?.type == 'CallExpression'
|
|
53
|
-
&& node.callee?.property?.name == 'createElement'
|
|
54
|
-
&& node.arguments[0]?.value == 'script'
|
|
55
|
-
}
|
|
56
|
-
function isLiteralURL(node) {
|
|
57
|
-
return node?.type == 'Literal'
|
|
58
|
-
&& typeof node.value == 'string'
|
|
59
|
-
&& regex.url.test(node.value)
|
|
60
|
-
}
|
|
61
|
-
function isObjectAssign(node) {
|
|
62
|
-
return node?.type == 'CallExpression'
|
|
63
|
-
&& node.callee?.type == 'MemberExpression'
|
|
64
|
-
&& node.callee.object?.name == 'Object'
|
|
65
|
-
&& node.callee.property?.name == 'assign'
|
|
66
|
-
}
|
|
67
84
|
function isScriptSrcAssignment(left, right) {
|
|
68
85
|
if (left.type != 'MemberExpression') return false
|
|
69
86
|
if (left.property?.name != 'src') return false
|
|
@@ -72,12 +89,10 @@ export default {
|
|
|
72
89
|
if (left.object.type == 'Identifier' && scriptElems.has(left.object.name)) return true
|
|
73
90
|
return false
|
|
74
91
|
}
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
if (isCreateScript(parent)) return true
|
|
80
|
-
if (isObjectAssign(parent) && isCreateScript(parent.arguments[0])) return true
|
|
92
|
+
function isScriptElement(node) {
|
|
93
|
+
if (!node) return false
|
|
94
|
+
if (isCreateScript(node)) return true
|
|
95
|
+
if (node.type == 'Identifier' && scriptElems.has(node.name)) return true
|
|
81
96
|
return false
|
|
82
97
|
}
|
|
83
98
|
return {
|
|
@@ -112,12 +127,31 @@ export default {
|
|
|
112
127
|
if (optsNode?.type != 'ObjectExpression') return
|
|
113
128
|
const urlProp = optsNode.properties.find(prop => prop.key?.name == 'url'),
|
|
114
129
|
funcProp = optsNode.properties.find(prop => prop.key?.name == 'func'),
|
|
115
|
-
|
|
130
|
+
hasArgs = optsNode.properties.some(prop => prop.key?.name == 'args')
|
|
116
131
|
if (patterns.has('execute-script-url') && urlProp)
|
|
117
132
|
context.report({ node: urlProp, messageId: 'executeScriptURL' })
|
|
118
|
-
if (checkClosures && funcProp && !
|
|
133
|
+
if (checkClosures && funcProp && !hasArgs)
|
|
119
134
|
context.report({ node: funcProp, messageId: 'executeScriptClosure' })
|
|
120
135
|
}
|
|
136
|
+
if (patterns.has('inline-event-handler')
|
|
137
|
+
&& node.callee.property?.name == 'setAttribute'
|
|
138
|
+
&& node.arguments[0]?.type == 'Literal'
|
|
139
|
+
&& typeof node.arguments[0].value == 'string'
|
|
140
|
+
&& re.inlineHandler.test(node.arguments[0].value)
|
|
141
|
+
) context.report({ node, messageId: 'inlineEventHandler',
|
|
142
|
+
data: { attr: node.arguments[0].value.toLowerCase() }})
|
|
143
|
+
if (patterns.has('inline-event-handler')
|
|
144
|
+
&& node.callee.property?.name == 'insertAdjacentHTML'
|
|
145
|
+
&& isInlineHandlerInString(node.arguments[1])
|
|
146
|
+
) context.report({ node: node.arguments[1], messageId: 'inlineEventHandler',
|
|
147
|
+
data: { attr: 'on*' }})
|
|
148
|
+
if (patterns.has('inline-script-body')
|
|
149
|
+
&& ['append', 'prepend'].includes(node.callee.property?.name)
|
|
150
|
+
&& isScriptElement(node.callee.object)
|
|
151
|
+
&& node.arguments[0]?.type == 'Literal'
|
|
152
|
+
&& typeof node.arguments[0].value == 'string'
|
|
153
|
+
&& node.arguments[0].value.trim().length > 0
|
|
154
|
+
) context.report({ node: node.arguments[0], messageId: 'inlineScriptBody' })
|
|
121
155
|
}
|
|
122
156
|
},
|
|
123
157
|
NewExpression(node) {
|
|
@@ -125,6 +159,21 @@ export default {
|
|
|
125
159
|
context.report({ node, messageId: 'newFunction' })
|
|
126
160
|
},
|
|
127
161
|
AssignmentExpression(node) {
|
|
162
|
+
if (patterns.has('inline-event-handler')
|
|
163
|
+
&& node.left.type == 'MemberExpression'
|
|
164
|
+
&& ['innerHTML', 'outerHTML'].includes(node.left.property?.name)
|
|
165
|
+
&& !isScriptElement(node.left.object)
|
|
166
|
+
&& isInlineHandlerInString(node.right)
|
|
167
|
+
) context.report({ node: node.right, messageId: 'inlineEventHandler',
|
|
168
|
+
data: { attr: 'on*' }})
|
|
169
|
+
if (patterns.has('inline-script-body')
|
|
170
|
+
&& node.left.type == 'MemberExpression'
|
|
171
|
+
&& ['textContent', 'innerText', 'innerHTML', 'text'].includes(node.left.property?.name)
|
|
172
|
+
&& isScriptElement(node.left.object)
|
|
173
|
+
&& node.right?.type == 'Literal'
|
|
174
|
+
&& typeof node.right.value == 'string'
|
|
175
|
+
&& node.right.value.trim().length > 0
|
|
176
|
+
) context.report({ node, messageId: 'inlineScriptBody' })
|
|
128
177
|
if (!patterns.has('remote-script-src')) return
|
|
129
178
|
if (isScriptSrcAssignment(node.left, node.right))
|
|
130
179
|
context.report({ node: node.right, messageId: 'remoteScriptSrc' })
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import array from '../utils/array.js'
|
|
2
|
+
import html from '../utils/html.js'
|
|
3
|
+
|
|
4
|
+
const re = { objectTag: /<(?:object|embed)\b([^>]*)>/gi }
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'problem',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Disallow remote `<object data>` and `<embed src>` in extension HTML',
|
|
11
|
+
category: 'Possible Errors',
|
|
12
|
+
html: true
|
|
13
|
+
},
|
|
14
|
+
defaultOptions: [{ allowedPrefixes: ['chrome-extension://'] }],
|
|
15
|
+
schema: [{
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
allowedPrefixes: {
|
|
19
|
+
oneOf: [
|
|
20
|
+
{ type: 'string', description: 'A single allowed prefix' },
|
|
21
|
+
{ type: 'array', items: { type: 'string' }, description: 'List of allowed prefixes' }
|
|
22
|
+
],
|
|
23
|
+
description: 'Additional prefixes to permit'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
additionalProperties: false
|
|
27
|
+
}],
|
|
28
|
+
messages: {
|
|
29
|
+
remoteObject: 'Remote `<{{tag}} {{attr}}="{{src}}">` is blocked by MV3\'s `object-src \'self\'`. '
|
|
30
|
+
+ 'Bundle the resource or use `chrome.runtime.getURL()`.'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
create(context) {
|
|
35
|
+
const opts = context.options[0] || {},
|
|
36
|
+
allowedPrefixes = array.to(opts.allowedPrefixes ?? ['chrome-extension://']),
|
|
37
|
+
text = context.sourceCode.text
|
|
38
|
+
let scanned = false
|
|
39
|
+
function scan() {
|
|
40
|
+
if (scanned) return
|
|
41
|
+
scanned = true
|
|
42
|
+
if (!html.isHTML(text)) return
|
|
43
|
+
for (const match of text.matchAll(re.objectTag)) {
|
|
44
|
+
const [full, attrs] = match,
|
|
45
|
+
tag = full.trimStart().toLowerCase().startsWith('<object') ? 'object' : 'embed',
|
|
46
|
+
attr = tag == 'object' ? 'data' : 'src',
|
|
47
|
+
val = html.pickAttrVal(attrs.match(tag == 'object' ? html.re.attr.data : html.re.attr.src))
|
|
48
|
+
if (val !== null && html.isRemote(val.trim(), allowedPrefixes))
|
|
49
|
+
context.report({ loc: html.offsetToLoc(text, match.index), messageId: 'remoteObject',
|
|
50
|
+
data: { tag, attr, src: val.trim() }})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { 'Program:exit': scan, 'Document:exit': scan, '*': scan }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import array from '../utils/array.js'
|
|
2
|
+
import regex from '../data/regex.js'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
function getPreludeURLnode(prelude) {
|
|
5
|
+
if (!prelude) return null
|
|
6
|
+
if (['Url', 'CSSUrl', 'String', 'CSSString'].includes(prelude.type))
|
|
7
|
+
return prelude
|
|
8
|
+
for (const child of prelude.children ?? [])
|
|
9
|
+
if (['Url', 'CSSUrl', 'String', 'CSSString'].includes(child.type))
|
|
10
|
+
return child
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
function getURLstr(node) {
|
|
14
|
+
if (!node) return null
|
|
15
|
+
if (typeof node == 'string') return node
|
|
16
|
+
if (typeof node.url == 'string') return node.url
|
|
17
|
+
if (typeof node.value == 'string') return node.value
|
|
18
|
+
if (node.value && typeof node.value == 'object' && typeof node.value.value == 'string')
|
|
19
|
+
return node.value.value
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
4
22
|
|
|
5
23
|
export default {
|
|
6
24
|
meta: {
|
|
@@ -46,24 +64,6 @@ export default {
|
|
|
46
64
|
allowedSchemes = new Set(array.to(opts.allowedSchemes ?? ['blob', 'chrome-extension', 'data'])),
|
|
47
65
|
patterns = new Set(array.to(opts.patterns ?? ['at-import', 'url'])),
|
|
48
66
|
handledURLs = new WeakSet()
|
|
49
|
-
function getPreludeURLnode(prelude) {
|
|
50
|
-
if (!prelude) return null
|
|
51
|
-
if (['Url', 'CSSUrl', 'String', 'CSSString'].includes(prelude.type))
|
|
52
|
-
return prelude
|
|
53
|
-
for (const child of prelude.children ?? [])
|
|
54
|
-
if (['Url', 'CSSUrl', 'String', 'CSSString'].includes(child.type))
|
|
55
|
-
return child
|
|
56
|
-
return null
|
|
57
|
-
}
|
|
58
|
-
function getURLstr(node) {
|
|
59
|
-
if (!node) return null
|
|
60
|
-
if (typeof node == 'string') return node
|
|
61
|
-
if (typeof node.url == 'string') return node.url
|
|
62
|
-
if (typeof node.value == 'string') return node.value
|
|
63
|
-
if (node.value && typeof node.value == 'object' && typeof node.value.value == 'string')
|
|
64
|
-
return node.value.value
|
|
65
|
-
return null
|
|
66
|
-
}
|
|
67
67
|
function handleAtrule(node) {
|
|
68
68
|
if (!node.prelude) return
|
|
69
69
|
if (node.name == 'import' && patterns.has('at-import')) {
|
|
@@ -90,7 +90,7 @@ export default {
|
|
|
90
90
|
}
|
|
91
91
|
function isRemote(str) {
|
|
92
92
|
if (typeof str != 'string') return false
|
|
93
|
-
const match =
|
|
93
|
+
const match = regex.scheme.exec(str)
|
|
94
94
|
if (!match) return false
|
|
95
95
|
if (match[0] == '//') return true
|
|
96
96
|
const scheme = (match[1] || '').toLowerCase()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import array from '../utils/array.js'
|
|
2
|
-
import
|
|
2
|
+
import node from '../utils/node.js'
|
|
3
3
|
|
|
4
4
|
const re = {
|
|
5
|
-
scrollbarPseudo: /^(?:-webkit-)?scrollbar(?:-[\w-]+)?$/i } //
|
|
5
|
+
scrollbarPseudo: /^(?:-webkit-)?scrollbar(?:-[\w-]+)?$/i } // oxlint-disable-line security/detect-unsafe-regex
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
8
|
meta: {
|
|
@@ -43,31 +43,29 @@ export default {
|
|
|
43
43
|
hasQualifier = false,
|
|
44
44
|
pastFirstCompound = false
|
|
45
45
|
for (const simple of selector.children ?? []) {
|
|
46
|
-
const type = plainType(simple)
|
|
46
|
+
const type = node.plainType(simple)
|
|
47
47
|
if (type == 'Combinator') { pastFirstCompound = true; continue }
|
|
48
48
|
if (type == 'PseudoElementSelector' && re.scrollbarPseudo.test(simple.name ?? '')) {
|
|
49
49
|
hasScrollbarPseudo = true
|
|
50
50
|
continue
|
|
51
51
|
}
|
|
52
52
|
if (pastFirstCompound) continue
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
hasQualifier = true
|
|
53
|
+
if (['AttributeSelector', 'ClassSelector', 'IdSelector'].includes(type)
|
|
54
|
+
|| (type == 'PseudoClassSelector' && (simple.name ?? '').toLowerCase().startsWith('host'))
|
|
55
|
+
) hasQualifier = true
|
|
57
56
|
}
|
|
58
57
|
if (hasScrollbarPseudo && !hasQualifier)
|
|
59
58
|
context.report({ node: selector, messageId: 'noScrollbarStyling', data: { selector: text }})
|
|
60
59
|
}
|
|
61
60
|
function getSelectorText(selector) {
|
|
62
|
-
const range = selector
|
|
63
|
-
if (range
|
|
61
|
+
const { range } = selector
|
|
62
|
+
if (range?.length == 2) return context.sourceCode.text.slice(range[0], range[1])
|
|
64
63
|
return context.sourceCode.getText(selector)
|
|
65
64
|
}
|
|
66
65
|
function handleRule(node) {
|
|
67
66
|
for (const selector of node.prelude?.children ?? [])
|
|
68
67
|
checkSelector(selector)
|
|
69
68
|
}
|
|
70
|
-
function plainType(node) { return node?.type?.replace(regex.prefix.css, '') ?? '' }
|
|
71
69
|
return { Rule: handleRule, CSSRule: handleRule }
|
|
72
70
|
}
|
|
73
71
|
}
|
|
@@ -58,7 +58,7 @@ export default {
|
|
|
58
58
|
return {
|
|
59
59
|
'Program:exit'() {
|
|
60
60
|
for (const key of hostKeys) {
|
|
61
|
-
const patterns = manifest[key] //
|
|
61
|
+
const patterns = manifest[key] // oxlint-disable-line security/detect-object-injection
|
|
62
62
|
if (!Array.isArray(patterns)) continue
|
|
63
63
|
for (const pattern of patterns)
|
|
64
64
|
if (isBroad(pattern))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import apiUtils from '../utils/api.js'
|
|
1
2
|
import extension from '../utils/extension.js'
|
|
2
3
|
import fs from 'node:fs'
|
|
3
4
|
import path from 'node:path'
|
|
@@ -14,15 +15,15 @@ function collectUsedAPIs(manifest, root) {
|
|
|
14
15
|
if (manifest.action?.default_popup) files.add(manifest.action.default_popup)
|
|
15
16
|
if (manifest.options_ui?.page) files.add(manifest.options_ui.page)
|
|
16
17
|
if (manifest.devtools_page) files.add(manifest.devtools_page)
|
|
17
|
-
const re = { /*
|
|
18
|
+
const re = { /* oxlint-disable-next-line security/detect-unsafe-regex */
|
|
18
19
|
chromeAPI: /\bchrome\.\w+(?:\.\w+)*/g }
|
|
19
20
|
const used = new Set()
|
|
20
21
|
for (const file of files) {
|
|
21
22
|
try {
|
|
22
|
-
const src = //
|
|
23
|
+
const src = // oxlint-disable-next-line security/detect-non-literal-fs-filename
|
|
23
24
|
fs.readFileSync(path.resolve(root, file), 'utf8')
|
|
24
25
|
for (const match of src.matchAll(re.chromeAPI)) used.add(match[0])
|
|
25
|
-
} catch {}
|
|
26
|
+
} catch {}
|
|
26
27
|
}
|
|
27
28
|
return used
|
|
28
29
|
}
|
|
@@ -54,7 +55,7 @@ export default {
|
|
|
54
55
|
if (perm == permission) { knownPerm = true ; break }
|
|
55
56
|
if (!knownPerm) return true
|
|
56
57
|
for (const api of usedAPIs)
|
|
57
|
-
if (
|
|
58
|
+
if (apiUtils.findPermission(api) == permission) return true
|
|
58
59
|
return false
|
|
59
60
|
}
|
|
60
61
|
if (!regex.filename.manifest.test(filename)) {
|
|
@@ -19,7 +19,7 @@ export default {
|
|
|
19
19
|
const { filename } = context
|
|
20
20
|
function check(node) {
|
|
21
21
|
const pkgRoot = pkg.findRoot(path.dirname(filename))
|
|
22
|
-
if (!pkgRoot || !license.find(pkgRoot)) //
|
|
22
|
+
if (!pkgRoot || !license.find(pkgRoot)) // oxlint-disable-line e18e/prefer-array-some
|
|
23
23
|
context.report({ node, messageId: 'missingFile' })
|
|
24
24
|
}
|
|
25
25
|
return { Program: check, Document: check }
|
package/src/rules/require-mv3.js
CHANGED
|
@@ -32,7 +32,8 @@ export default {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
const manifest = extension.manifest.read(context)
|
|
36
|
-
|
|
35
|
+
const manifest = extension.manifest.read(context)
|
|
36
|
+
if (!manifest || manifest.manifest_version == 3) return {}
|
|
37
|
+
return { Program(node) { context.report({ node, messageId: 'notMV3' })}}
|
|
37
38
|
}
|
|
38
39
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* oxlint-disable security/detect-non-literal-fs-filename -- lint-time project inspection only */
|
|
2
2
|
|
|
3
3
|
import extension from '../utils/extension.js'
|
|
4
4
|
import fs from 'node:fs'
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
|
|
7
|
-
const re = { invalidChars: /[^\w\-.]
|
|
7
|
+
const re = { invalidChars: /[^\w\-.]/ }
|
|
8
8
|
|
|
9
9
|
const refKeys = [
|
|
10
10
|
['action.default_popup', manifest => manifest.action?.default_popup],
|
|
@@ -112,7 +112,7 @@ export default {
|
|
|
112
112
|
segments = path.relative(extRoot, filename).split(path.sep)
|
|
113
113
|
if (noReservedPrefix)
|
|
114
114
|
for (const seg of segments)
|
|
115
|
-
if (
|
|
115
|
+
if (seg.startsWith('_') && seg != '_locales')
|
|
116
116
|
context.report({ loc: { line: 1, column: 1 }, messageId: 'reservedPrefix', data: { name: seg }})
|
|
117
117
|
if (noSpecialChars)
|
|
118
118
|
for (const seg of segments)
|
|
@@ -20,7 +20,7 @@ function validateIcons(context, icons, root, opts, loc) {
|
|
|
20
20
|
if (typeof file != 'string') continue
|
|
21
21
|
if (checkSVG && re.svg.test(file))
|
|
22
22
|
context.report({ loc, messageId: 'svgNotSupported' })
|
|
23
|
-
else if (checkFiles && root //
|
|
23
|
+
else if (checkFiles && root // oxlint-disable-next-line security/detect-non-literal-fs-filename
|
|
24
24
|
&& !fs.existsSync(path.resolve(root, file))
|
|
25
25
|
) context.report({ loc, messageId: 'missingFile', data: { file }})
|
|
26
26
|
}
|
|
@@ -98,7 +98,7 @@ export default {
|
|
|
98
98
|
if (typeof file != 'string') continue
|
|
99
99
|
if (checkSVG && re.svg.test(file))
|
|
100
100
|
context.report({ node: fileNode, messageId: 'svgNotSupported' })
|
|
101
|
-
else if (checkFiles && root //
|
|
101
|
+
else if (checkFiles && root // oxlint-disable-next-line security/detect-non-literal-fs-filename
|
|
102
102
|
&& !fs.existsSync(path.resolve(root, file))
|
|
103
103
|
) context.report({ node: fileNode, messageId: 'missingFile', data: { file }})
|
|
104
104
|
}
|
|
@@ -85,10 +85,10 @@ export default {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
const manifest = extension.manifest.read(context) ; if (
|
|
88
|
+
const manifest = extension.manifest.read(context) ; if (manifest?.manifest_version != 3) return {}
|
|
89
89
|
const isFF = manifest.browser_specific_settings != null
|
|
90
|
-
return {
|
|
91
|
-
Program(node) {
|
|
90
|
+
return { // oxlint-disable security/detect-object-injection
|
|
91
|
+
Program(node) {
|
|
92
92
|
for (const key of Object.keys(manifest))
|
|
93
93
|
if (manifestData.keys.mv2Only.has(key))
|
|
94
94
|
context.report({ node, messageId: 'mv2Key', data: { key, reason: 'MV2-only key' }})
|
|
@@ -117,7 +117,7 @@ export default {
|
|
|
117
117
|
if (!set.has(value))
|
|
118
118
|
context.report({ node, messageId: 'invalidDataCollectionValue', data: { value, kind }})
|
|
119
119
|
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
120
|
+
}
|
|
121
|
+
} // oxlint-enable security/detect-object-injection
|
|
122
122
|
}
|
|
123
123
|
}
|