eslint-plugin-vercel-ai-security 1.3.9 → 1.4.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/CHANGELOG.md +60 -0
- package/package.json +22 -14
- package/src/index.js +1 -1
- package/src/rules/no-dynamic-system-prompt/index.js +5 -4
- package/src/rules/no-sensitive-in-prompt/index.js +5 -4
- package/src/rules/require-rag-content-validation/index.js +4 -3
- package/src/rules/require-validated-prompt/index.js +4 -7
- package/src/utils/prompt-props.d.ts +27 -0
- package/src/utils/prompt-props.js +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,63 @@
|
|
|
1
|
+
## 1.4.0
|
|
2
|
+
|
|
3
|
+
### Minor Changes
|
|
4
|
+
|
|
5
|
+
- [#299](https://github.com/ofri-peretz/eslint/pull/299) [`608defb`](https://github.com/ofri-peretz/eslint/commit/608defbbce6818e2ad4f51d7425e044c4ca04e3e) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Detect the AI SDK v7 `instructions` option, not just the deprecated `system`.
|
|
6
|
+
|
|
7
|
+
AI SDK v7 renamed the system-prompt option to `instructions` and marks `system` as
|
|
8
|
+
`@deprecated Use 'instructions' instead` in its own type definitions. Four rules matched
|
|
9
|
+
the property name `system` literally, so on any code written against current AI SDK docs
|
|
10
|
+
they reported nothing at all:
|
|
11
|
+
|
|
12
|
+
- `no-dynamic-system-prompt`
|
|
13
|
+
- `require-validated-prompt` (its `unsafeSystemPrompt` branch)
|
|
14
|
+
- `require-rag-content-validation`
|
|
15
|
+
- `no-sensitive-in-prompt`
|
|
16
|
+
|
|
17
|
+
All four now accept either spelling via a shared `SYSTEM_PROMPT_PROPS` set, so `system`
|
|
18
|
+
keeps working for pre-v7 code and `instructions` is covered going forward.
|
|
19
|
+
|
|
20
|
+
The same pass closed a second silent miss in those rules: a quoted key
|
|
21
|
+
(`{ "instructions": … }`) parses to a string `Literal`, not an `Identifier`, and
|
|
22
|
+
three of the four rules read only `Identifier` keys — so putting quotes round the
|
|
23
|
+
key was enough to stop them firing. Key extraction now goes through a shared
|
|
24
|
+
`getStaticPropName` helper. Three fixtures that recorded this as expected
|
|
25
|
+
behaviour (one labelled "documented FN: only Identifier keys are matched") moved
|
|
26
|
+
from `valid` to `invalid`, since a rule that stops firing on formatting is a miss,
|
|
27
|
+
not a design decision. Computed keys (`{ [k]: … }`) still return `null` — there
|
|
28
|
+
the name genuinely isn't statically known.
|
|
29
|
+
|
|
30
|
+
This was found the hard way. Scanning the `nuxt-ui-templates/chat` template, a system
|
|
31
|
+
prompt interpolating the signed-in user's name straight into `instructions:` went
|
|
32
|
+
unreported — the finding was spotted by reading the file, not by the linter that exists to
|
|
33
|
+
catch it. Each of the four rules now carries a regression test that fails if the
|
|
34
|
+
`instructions` spelling is dropped again, and one of them uses the exact template-literal
|
|
35
|
+
shape from that file.
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- Updated dependencies [[`09d2951`](https://github.com/ofri-peretz/eslint/commit/09d2951b3ac74efc9ba49b64e9089d66800b16cc)]:
|
|
40
|
+
- @interlace/eslint-devkit@1.4.4
|
|
41
|
+
|
|
42
|
+
## 1.3.10
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- [#294](https://github.com/ofri-peretz/eslint/pull/294) [`659f6dc`](https://github.com/ofri-peretz/eslint/commit/659f6dc0181b03b675f72b5949fcf123dd066358) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Rewrite `description` and `keywords` on every published package for npm search discovery. npm ranks on name, description, and keywords, and the registry only picks up these fields at publish — so this is metadata-only and takes effect for each package on its next release.
|
|
47
|
+
|
|
48
|
+
**Descriptions now lead with the search phrase.** Every one starts `ESLint plugin for <the thing you'd search>` instead of a brand-first or category-first framing, and names the concrete vulnerabilities the plugin actually detects. Three were corrected while doing so:
|
|
49
|
+
|
|
50
|
+
- `eslint-plugin-import-next` claimed "100x faster no-cycle detection". No 100x measurement exists: `CLAIMS.md` records **3.1x end-to-end** (8x in pure rule execution) on a 5,483-file React codebase, and the highest number in any benchmark result is 54.9x on the synthetic corpus. The description now states the real-codebase figure.
|
|
51
|
+
- `eslint-plugin-secure-coding` claimed SQL injection, XSS and CSRF coverage — none of which are its rules. It now names what it does detect: LDAP, XPath, XXE, GraphQL and template injection, unsafe deserialization, ReDoS, missing authentication, and PII in logs.
|
|
52
|
+
- `eslint-plugin-secure-coding` ("89 rules") and `eslint-plugin-react-a11y` ("37 rules") hard-coded rule counts that had drifted from reality. Counts are generated into `interlace-numbers.json`; hand-typed copies are removed rather than corrected.
|
|
53
|
+
|
|
54
|
+
**Keywords now match the vocabulary of the plugins that rank.** `eslint-plugin-security`, `eslint-plugin-jsx-a11y`, `eslint-plugin-n` and `eslint-plugin-import` all carry the `eslint` / `eslintplugin` / `eslint-plugin` trio — six of our packages were missing `eslintplugin`, and every one now carries all three plus `static-analysis`, `linting` and `code-quality`. Security plugins add `sast`, `appsec` and `vulnerability`; `node-security` and `secure-coding` also carry `nodesecurity`, the exact keyword `eslint-plugin-security` ranks on. Each plugin gained the CWE identifiers and attack names for what it detects (`cwe-78` command injection, `cwe-22` path traversal, `cwe-89` SQL injection, `cwe-79` XSS, `cwe-347` JWT algorithm confusion, `cwe-352` CSRF, `cwe-943` NoSQL injection), and `node-security` gained the crypto vocabulary it had been missing entirely despite absorbing the crypto rule set (`crypto`, `cryptography`, `weak-hash`, `md5`, `sha1`, `timing-attack`).
|
|
55
|
+
|
|
56
|
+
No rule behavior, exports, or configuration changes.
|
|
57
|
+
|
|
58
|
+
- Updated dependencies [[`e1cdf83`](https://github.com/ofri-peretz/eslint/commit/e1cdf83e3db761907f0ab06f7fc6c1f1da7513a5), [`659f6dc`](https://github.com/ofri-peretz/eslint/commit/659f6dc0181b03b675f72b5949fcf123dd066358)]:
|
|
59
|
+
- @interlace/eslint-devkit@1.4.3
|
|
60
|
+
|
|
1
61
|
## 1.3.9
|
|
2
62
|
|
|
3
63
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-vercel-ai-security",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "ESLint plugin for Vercel AI SDK security — detects prompt injection, system-prompt leaks, hardcoded API keys, and unvalidated model output in generateText and streamText.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./src/index.d.ts",
|
|
@@ -43,27 +43,35 @@
|
|
|
43
43
|
],
|
|
44
44
|
"keywords": [
|
|
45
45
|
"eslint",
|
|
46
|
-
"eslint-plugin",
|
|
47
46
|
"eslintplugin",
|
|
48
|
-
"
|
|
47
|
+
"eslint-plugin",
|
|
48
|
+
"static-analysis",
|
|
49
|
+
"linting",
|
|
50
|
+
"code-quality",
|
|
49
51
|
"security",
|
|
52
|
+
"sast",
|
|
53
|
+
"appsec",
|
|
54
|
+
"vulnerability",
|
|
55
|
+
"owasp",
|
|
56
|
+
"cwe",
|
|
57
|
+
"interlace-security",
|
|
50
58
|
"vercel",
|
|
51
59
|
"vercel-ai",
|
|
60
|
+
"vercel-ai-sdk",
|
|
52
61
|
"ai-sdk",
|
|
53
|
-
"
|
|
54
|
-
"streamText",
|
|
62
|
+
"ai-security",
|
|
55
63
|
"llm",
|
|
64
|
+
"llm-security",
|
|
65
|
+
"generative-ai",
|
|
56
66
|
"prompt-injection",
|
|
57
|
-
"owasp",
|
|
58
67
|
"owasp-llm-top-10",
|
|
59
|
-
"
|
|
60
|
-
"
|
|
68
|
+
"rag",
|
|
69
|
+
"tool-calling",
|
|
70
|
+
"generateText",
|
|
71
|
+
"streamText",
|
|
61
72
|
"llm-optimized",
|
|
62
73
|
"ai-assistant",
|
|
63
|
-
"typescript"
|
|
64
|
-
"linting",
|
|
65
|
-
"code-quality",
|
|
66
|
-
"static-analysis"
|
|
74
|
+
"typescript"
|
|
67
75
|
],
|
|
68
76
|
"engines": {
|
|
69
77
|
"node": ">=18.0.0"
|
|
@@ -78,7 +86,7 @@
|
|
|
78
86
|
},
|
|
79
87
|
"dependencies": {
|
|
80
88
|
"tslib": "^2.3.0",
|
|
81
|
-
"@interlace/eslint-devkit": "^1.4.
|
|
89
|
+
"@interlace/eslint-devkit": "^1.4.4"
|
|
82
90
|
},
|
|
83
91
|
"devDependencies": {
|
|
84
92
|
"@ai-sdk/openai": "^4.0.16",
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,7 @@ exports.noDynamicSystemPrompt = void 0;
|
|
|
12
12
|
* @see OWASP ASI01: Agent Confusion
|
|
13
13
|
*/
|
|
14
14
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
15
|
+
const prompt_props_1 = require("../../utils/prompt-props");
|
|
15
16
|
exports.noDynamicSystemPrompt = (0, eslint_devkit_1.createRule)({
|
|
16
17
|
name: 'no-dynamic-system-prompt',
|
|
17
18
|
meta: {
|
|
@@ -96,12 +97,12 @@ exports.noDynamicSystemPrompt = (0, eslint_devkit_1.createRule)({
|
|
|
96
97
|
const optionsArg = node.arguments[0];
|
|
97
98
|
if (!optionsArg || optionsArg.type !== 'ObjectExpression')
|
|
98
99
|
return;
|
|
99
|
-
// Find system property
|
|
100
|
+
// Find the system-prompt property (`instructions` in AI SDK v7+, `system` before it)
|
|
100
101
|
for (const prop of optionsArg.properties) {
|
|
101
|
-
if (prop.type !==
|
|
102
|
+
if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
|
|
102
103
|
continue;
|
|
103
|
-
const keyName =
|
|
104
|
-
if (
|
|
104
|
+
const keyName = (0, prompt_props_1.getStaticPropName)(prop.key);
|
|
105
|
+
if (!(0, prompt_props_1.isSystemPromptProp)(keyName))
|
|
105
106
|
continue;
|
|
106
107
|
// Check if system prompt is dynamic
|
|
107
108
|
if (isDynamicContent(prop.value)) {
|
|
@@ -12,6 +12,7 @@ exports.noSensitiveInPrompt = void 0;
|
|
|
12
12
|
* @see OWASP LLM02: Sensitive Information Disclosure
|
|
13
13
|
*/
|
|
14
14
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
15
|
+
const prompt_props_1 = require("../../utils/prompt-props");
|
|
15
16
|
exports.noSensitiveInPrompt = (0, eslint_devkit_1.createRule)({
|
|
16
17
|
name: 'no-sensitive-in-prompt',
|
|
17
18
|
meta: {
|
|
@@ -117,12 +118,12 @@ exports.noSensitiveInPrompt = (0, eslint_devkit_1.createRule)({
|
|
|
117
118
|
const optionsArg = node.arguments[0];
|
|
118
119
|
if (!optionsArg || optionsArg.type !== 'ObjectExpression')
|
|
119
120
|
return;
|
|
120
|
-
// Check prompt, system, and messages
|
|
121
|
-
const propsToCheck = new Set(['prompt', '
|
|
121
|
+
// Check prompt, the system-prompt props, and messages
|
|
122
|
+
const propsToCheck = new Set(['prompt', 'messages', ...prompt_props_1.SYSTEM_PROMPT_PROPS]);
|
|
122
123
|
for (const prop of optionsArg.properties) {
|
|
123
|
-
if (prop.type !==
|
|
124
|
+
if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
|
|
124
125
|
continue;
|
|
125
|
-
const keyName =
|
|
126
|
+
const keyName = (0, prompt_props_1.getStaticPropName)(prop.key);
|
|
126
127
|
if (!keyName || !propsToCheck.has(keyName))
|
|
127
128
|
continue;
|
|
128
129
|
const sensitiveVar = findSensitiveData(prop.value);
|
|
@@ -12,6 +12,7 @@ exports.requireRagContentValidation = void 0;
|
|
|
12
12
|
* @see OWASP ASI07: Poisoned RAG Pipeline
|
|
13
13
|
*/
|
|
14
14
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
15
|
+
const prompt_props_1 = require("../../utils/prompt-props");
|
|
15
16
|
exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
|
|
16
17
|
name: 'require-rag-content-validation',
|
|
17
18
|
meta: {
|
|
@@ -157,10 +158,10 @@ exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
|
|
|
157
158
|
return;
|
|
158
159
|
// Check prompt property
|
|
159
160
|
for (const prop of optionsArg.properties) {
|
|
160
|
-
if (prop.type !==
|
|
161
|
+
if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
|
|
161
162
|
continue;
|
|
162
|
-
const keyName =
|
|
163
|
-
if (keyName !== 'prompt' &&
|
|
163
|
+
const keyName = (0, prompt_props_1.getStaticPropName)(prop.key);
|
|
164
|
+
if (keyName !== 'prompt' && !(0, prompt_props_1.isSystemPromptProp)(keyName))
|
|
164
165
|
continue;
|
|
165
166
|
const ragSource = containsRagContent(prop.value);
|
|
166
167
|
if (ragSource) {
|
|
@@ -13,6 +13,7 @@ exports.requireValidatedPrompt = void 0;
|
|
|
13
13
|
* @see https://owasp.org/www-project-top-10-for-large-language-model-applications/
|
|
14
14
|
*/
|
|
15
15
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
16
|
+
const prompt_props_1 = require("../../utils/prompt-props");
|
|
16
17
|
exports.requireValidatedPrompt = (0, eslint_devkit_1.createRule)({
|
|
17
18
|
name: 'require-validated-prompt',
|
|
18
19
|
meta: {
|
|
@@ -192,13 +193,9 @@ exports.requireValidatedPrompt = (0, eslint_devkit_1.createRule)({
|
|
|
192
193
|
}
|
|
193
194
|
// Check for unsafe prompt property
|
|
194
195
|
for (const prop of optionsArg.properties) {
|
|
195
|
-
if (prop.type !==
|
|
196
|
+
if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
|
|
196
197
|
continue;
|
|
197
|
-
const keyName = prop.key
|
|
198
|
-
? prop.key.name
|
|
199
|
-
: prop.key.type === 'Literal'
|
|
200
|
-
? String(prop.key.value)
|
|
201
|
-
: null;
|
|
198
|
+
const keyName = (0, prompt_props_1.getStaticPropName)(prop.key);
|
|
202
199
|
if (keyName === 'prompt') {
|
|
203
200
|
const result = hasUnsafeUserInput(prop.value);
|
|
204
201
|
if (result.unsafe) {
|
|
@@ -212,7 +209,7 @@ exports.requireValidatedPrompt = (0, eslint_devkit_1.createRule)({
|
|
|
212
209
|
});
|
|
213
210
|
}
|
|
214
211
|
}
|
|
215
|
-
if (
|
|
212
|
+
if ((0, prompt_props_1.isSystemPromptProp)(keyName)) {
|
|
216
213
|
const result = hasUnsafeUserInput(prop.value);
|
|
217
214
|
if (result.unsafe) {
|
|
218
215
|
context.report({
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
import { type TSESTree } from '@interlace/eslint-devkit';
|
|
7
|
+
/**
|
|
8
|
+
* Property names the AI SDK accepts for the system prompt.
|
|
9
|
+
*
|
|
10
|
+
* As of AI SDK v7 the canonical name is `instructions`; `system` still works but
|
|
11
|
+
* is marked `@deprecated Use 'instructions' instead` in the SDK's own types. Both
|
|
12
|
+
* must be checked — code written against current docs uses `instructions`, and
|
|
13
|
+
* code predating v7 uses `system`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const SYSTEM_PROMPT_PROPS: ReadonlySet<string>;
|
|
16
|
+
/** True when `name` is a property the AI SDK treats as the system prompt. */
|
|
17
|
+
export declare function isSystemPromptProp(name: string | null): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Static name of an object property key, or `null` when it isn't statically known.
|
|
20
|
+
*
|
|
21
|
+
* `{ instructions: x }` and `{ 'instructions': x }` are the same property, but the
|
|
22
|
+
* first parses to an `Identifier` key and the second to a string `Literal`. Reading
|
|
23
|
+
* only `Identifier` silently skips the quoted form, so a rule stops firing on code
|
|
24
|
+
* that is semantically identical. Computed keys (`{ [k]: x }`) stay `null` — the
|
|
25
|
+
* name genuinely isn't known statically.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getStaticPropName(key: TSESTree.Node): string | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.SYSTEM_PROMPT_PROPS = void 0;
|
|
9
|
+
exports.isSystemPromptProp = isSystemPromptProp;
|
|
10
|
+
exports.getStaticPropName = getStaticPropName;
|
|
11
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
12
|
+
/**
|
|
13
|
+
* Property names the AI SDK accepts for the system prompt.
|
|
14
|
+
*
|
|
15
|
+
* As of AI SDK v7 the canonical name is `instructions`; `system` still works but
|
|
16
|
+
* is marked `@deprecated Use 'instructions' instead` in the SDK's own types. Both
|
|
17
|
+
* must be checked — code written against current docs uses `instructions`, and
|
|
18
|
+
* code predating v7 uses `system`.
|
|
19
|
+
*/
|
|
20
|
+
exports.SYSTEM_PROMPT_PROPS = new Set(['instructions', 'system']);
|
|
21
|
+
/** True when `name` is a property the AI SDK treats as the system prompt. */
|
|
22
|
+
function isSystemPromptProp(name) {
|
|
23
|
+
return name !== null && exports.SYSTEM_PROMPT_PROPS.has(name);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Static name of an object property key, or `null` when it isn't statically known.
|
|
27
|
+
*
|
|
28
|
+
* `{ instructions: x }` and `{ 'instructions': x }` are the same property, but the
|
|
29
|
+
* first parses to an `Identifier` key and the second to a string `Literal`. Reading
|
|
30
|
+
* only `Identifier` silently skips the quoted form, so a rule stops firing on code
|
|
31
|
+
* that is semantically identical. Computed keys (`{ [k]: x }`) stay `null` — the
|
|
32
|
+
* name genuinely isn't known statically.
|
|
33
|
+
*/
|
|
34
|
+
function getStaticPropName(key) {
|
|
35
|
+
if (key.type === eslint_devkit_1.AST_NODE_TYPES.Identifier)
|
|
36
|
+
return key.name;
|
|
37
|
+
if (key.type === eslint_devkit_1.AST_NODE_TYPES.Literal && typeof key.value === 'string')
|
|
38
|
+
return key.value;
|
|
39
|
+
return null;
|
|
40
|
+
}
|