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 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.3.9",
4
- "description": "Security-focused ESLint plugin for Vercel AI SDK. SDK-aware rules for generateText, streamText, tools, and streaming patterns with full type knowledge.",
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
- "interlace-security",
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
- "generateText",
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
- "cwe",
60
- "vulnerability",
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.2"
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
@@ -152,7 +152,7 @@ exports.rules = {
152
152
  exports.plugin = {
153
153
  meta: {
154
154
  name: 'eslint-plugin-vercel-ai-security',
155
- version: '1.3.9',
155
+ version: '1.4.0',
156
156
  },
157
157
  rules: exports.rules,
158
158
  };
@@ -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 !== 'Property')
102
+ if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
102
103
  continue;
103
- const keyName = prop.key.type === 'Identifier' ? prop.key.name : null;
104
- if (keyName !== 'system')
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 properties
121
- const propsToCheck = new Set(['prompt', 'system', 'messages']);
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 !== 'Property')
124
+ if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
124
125
  continue;
125
- const keyName = prop.key.type === 'Identifier' ? prop.key.name : null;
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 !== 'Property')
161
+ if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
161
162
  continue;
162
- const keyName = prop.key.type === 'Identifier' ? prop.key.name : null;
163
- if (keyName !== 'prompt' && keyName !== 'system')
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 !== 'Property')
196
+ if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
196
197
  continue;
197
- const keyName = prop.key.type === 'Identifier'
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 (keyName === 'system') {
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
+ }