eslint-plugin-vercel-ai-security 1.3.10 → 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 +41 -0
- package/package.json +2 -2
- 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,44 @@
|
|
|
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
|
+
|
|
1
42
|
## 1.3.10
|
|
2
43
|
|
|
3
44
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-vercel-ai-security",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
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",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
88
|
"tslib": "^2.3.0",
|
|
89
|
-
"@interlace/eslint-devkit": "^1.4.
|
|
89
|
+
"@interlace/eslint-devkit": "^1.4.4"
|
|
90
90
|
},
|
|
91
91
|
"devDependencies": {
|
|
92
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
|
+
}
|