eslint-plugin-vercel-ai-security 1.4.0 → 1.4.2
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/package.json +5 -20
- package/src/index.d.ts +17 -74
- package/src/index.js +1 -99
- package/src/oxlint.d.ts +17 -20
- package/src/oxlint.js +0 -18
- package/src/rules/no-dynamic-system-prompt/index.js +1 -23
- package/src/rules/no-hardcoded-api-keys/index.js +5 -28
- package/src/rules/no-sensitive-in-prompt/index.js +1 -21
- package/src/rules/no-system-prompt-leak/index.js +0 -25
- package/src/rules/no-training-data-exposure/index.js +0 -16
- package/src/rules/no-unsafe-output-handling/index.js +0 -35
- package/src/rules/require-abort-signal/index.js +0 -14
- package/src/rules/require-audit-logging/index.js +0 -24
- package/src/rules/require-embedding-validation/index.js +0 -23
- package/src/rules/require-error-handling/index.js +0 -19
- package/src/rules/require-max-steps/index.js +0 -18
- package/src/rules/require-max-tokens/index.js +0 -14
- package/src/rules/require-output-filtering/index.js +0 -23
- package/src/rules/require-output-validation/index.js +0 -23
- package/src/rules/require-rag-content-validation/index.js +1 -29
- package/src/rules/require-request-timeout/index.js +0 -16
- package/src/rules/require-tool-confirmation/index.js +1 -25
- package/src/rules/require-tool-schema/index.js +0 -19
- package/src/rules/require-validated-prompt/index.js +1 -34
- package/src/types/index.js +0 -5
- package/src/utils/prompt-props.js +8 -28
- package/AGENTS.md +0 -131
- package/CHANGELOG.md +0 -255
- package/src/rules/no-dynamic-system-prompt/index.d.ts +0 -14
- package/src/rules/no-hardcoded-api-keys/index.d.ts +0 -14
- package/src/rules/no-sensitive-in-prompt/index.d.ts +0 -14
- package/src/rules/no-system-prompt-leak/index.d.ts +0 -14
- package/src/rules/no-training-data-exposure/index.d.ts +0 -14
- package/src/rules/no-unsafe-output-handling/index.d.ts +0 -15
- package/src/rules/require-abort-signal/index.d.ts +0 -14
- package/src/rules/require-audit-logging/index.d.ts +0 -14
- package/src/rules/require-embedding-validation/index.d.ts +0 -16
- package/src/rules/require-error-handling/index.d.ts +0 -14
- package/src/rules/require-max-steps/index.d.ts +0 -14
- package/src/rules/require-max-tokens/index.d.ts +0 -16
- package/src/rules/require-output-filtering/index.d.ts +0 -16
- package/src/rules/require-output-validation/index.d.ts +0 -16
- package/src/rules/require-rag-content-validation/index.d.ts +0 -16
- package/src/rules/require-request-timeout/index.d.ts +0 -14
- package/src/rules/require-tool-confirmation/index.d.ts +0 -14
- package/src/rules/require-tool-schema/index.d.ts +0 -15
- package/src/rules/require-validated-prompt/index.d.ts +0 -19
- package/src/utils/prompt-props.d.ts +0 -27
|
@@ -1,16 +1,6 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.requireRagContentValidation = void 0;
|
|
9
|
-
/**
|
|
10
|
-
* @fileoverview Require validation of RAG content before use in prompts
|
|
11
|
-
* @description Detects when retrieved documents are used directly without sanitization
|
|
12
|
-
* @see OWASP ASI07: Poisoned RAG Pipeline
|
|
13
|
-
*/
|
|
14
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
15
5
|
const prompt_props_1 = require("../../utils/prompt-props");
|
|
16
6
|
exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
|
|
@@ -78,13 +68,8 @@ exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
|
|
|
78
68
|
'validate', 'sanitize', 'filter', 'clean',
|
|
79
69
|
];
|
|
80
70
|
const sourceCode = context.sourceCode;
|
|
81
|
-
// Vercel AI SDK functions
|
|
82
71
|
const aiSDKFunctions = ['generateText', 'streamText', 'generateObject', 'streamObject'];
|
|
83
|
-
// Track variables that hold RAG content
|
|
84
72
|
const ragVariables = new Set();
|
|
85
|
-
/**
|
|
86
|
-
* Check if expression is a RAG retrieval call
|
|
87
|
-
*/
|
|
88
73
|
function isRagCall(node) {
|
|
89
74
|
if (node.type !== 'CallExpression')
|
|
90
75
|
return null;
|
|
@@ -96,16 +81,10 @@ exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
|
|
|
96
81
|
}
|
|
97
82
|
return null;
|
|
98
83
|
}
|
|
99
|
-
/**
|
|
100
|
-
* Check if expression is wrapped in validation
|
|
101
|
-
*/
|
|
102
84
|
function isValidated(node) {
|
|
103
85
|
const callee = sourceCode.getText(node.callee);
|
|
104
86
|
return validatorFunctions.some((fn) => callee.toLowerCase().includes(fn.toLowerCase()));
|
|
105
87
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Check if an expression contains RAG content
|
|
108
|
-
*/
|
|
109
88
|
function containsRagContent(node) {
|
|
110
89
|
if (node.type === 'Identifier' && ragVariables.has(node.name)) {
|
|
111
90
|
return node.name;
|
|
@@ -117,12 +96,10 @@ exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
|
|
|
117
96
|
return rag;
|
|
118
97
|
}
|
|
119
98
|
}
|
|
120
|
-
// Handle: ${await retrieve(query)}
|
|
121
99
|
if (node.type === 'AwaitExpression') {
|
|
122
100
|
return containsRagContent(node.argument);
|
|
123
101
|
}
|
|
124
102
|
if (node.type === 'CallExpression') {
|
|
125
|
-
// Check if this is an unvalidated RAG call
|
|
126
103
|
const ragSource = isRagCall(node);
|
|
127
104
|
if (ragSource && !isValidated(node)) {
|
|
128
105
|
return ragSource;
|
|
@@ -131,13 +108,11 @@ exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
|
|
|
131
108
|
return null;
|
|
132
109
|
}
|
|
133
110
|
return {
|
|
134
|
-
// Track RAG variable assignments
|
|
135
111
|
VariableDeclarator(node) {
|
|
136
112
|
if (node.id.type !== 'Identifier')
|
|
137
113
|
return;
|
|
138
114
|
if (!node.init)
|
|
139
115
|
return;
|
|
140
|
-
// Handle: const docs = await vectorStore.search(query);
|
|
141
116
|
let initNode = node.init;
|
|
142
117
|
if (initNode.type === 'AwaitExpression') {
|
|
143
118
|
initNode = initNode.argument;
|
|
@@ -148,19 +123,16 @@ exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
|
|
|
148
123
|
},
|
|
149
124
|
CallExpression(node) {
|
|
150
125
|
const callee = sourceCode.getText(node.callee);
|
|
151
|
-
// Check if this is an AI SDK function
|
|
152
126
|
const isAIFunction = aiSDKFunctions.some(fn => callee.includes(fn));
|
|
153
127
|
if (!isAIFunction)
|
|
154
128
|
return;
|
|
155
|
-
// Check first argument (options object)
|
|
156
129
|
const optionsArg = node.arguments[0];
|
|
157
130
|
if (!optionsArg || optionsArg.type !== 'ObjectExpression')
|
|
158
131
|
return;
|
|
159
|
-
// Check prompt property
|
|
160
132
|
for (const prop of optionsArg.properties) {
|
|
161
133
|
if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
|
|
162
134
|
continue;
|
|
163
|
-
const keyName = (0, prompt_props_1.getStaticPropName)(prop
|
|
135
|
+
const keyName = (0, prompt_props_1.getStaticPropName)(prop);
|
|
164
136
|
if (keyName !== 'prompt' && !(0, prompt_props_1.isSystemPromptProp)(keyName))
|
|
165
137
|
continue;
|
|
166
138
|
const ragSource = containsRagContent(prop.value);
|
|
@@ -1,16 +1,6 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.requireRequestTimeout = void 0;
|
|
9
|
-
/**
|
|
10
|
-
* @fileoverview Require timeout for AI API calls to prevent DoS
|
|
11
|
-
* @description Detects AI calls without timeout configuration
|
|
12
|
-
* @see OWASP LLM04: Model Denial of Service
|
|
13
|
-
*/
|
|
14
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
15
5
|
exports.requireRequestTimeout = (0, eslint_devkit_1.createRule)({
|
|
16
6
|
name: 'require-request-timeout',
|
|
@@ -59,22 +49,17 @@ exports.requireRequestTimeout = (0, eslint_devkit_1.createRule)({
|
|
|
59
49
|
const allowInTests = options.allowInTests ?? true;
|
|
60
50
|
const sourceCode = context.sourceCode;
|
|
61
51
|
const filename = context.filename;
|
|
62
|
-
// Skip test files if allowed
|
|
63
52
|
if (allowInTests && /\.(test|spec)\.[jt]sx?$/.test(filename)) {
|
|
64
53
|
return {};
|
|
65
54
|
}
|
|
66
|
-
// Vercel AI SDK functions
|
|
67
55
|
const aiSDKFunctions = ['generateText', 'streamText', 'generateObject', 'streamObject'];
|
|
68
|
-
// Timeout-related property names
|
|
69
56
|
const timeoutProperties = new Set(['timeout', 'abortSignal', 'signal', 'timeoutMs', 'requestTimeout']);
|
|
70
57
|
return {
|
|
71
58
|
CallExpression(node) {
|
|
72
59
|
const callee = sourceCode.getText(node.callee);
|
|
73
|
-
// Check if this is an AI SDK function
|
|
74
60
|
const matchedFunction = aiSDKFunctions.find(fn => callee.includes(fn));
|
|
75
61
|
if (!matchedFunction)
|
|
76
62
|
return;
|
|
77
|
-
// Check first argument (options object)
|
|
78
63
|
const optionsArg = node.arguments[0];
|
|
79
64
|
if (!optionsArg || optionsArg.type !== 'ObjectExpression') {
|
|
80
65
|
context.report({
|
|
@@ -84,7 +69,6 @@ exports.requireRequestTimeout = (0, eslint_devkit_1.createRule)({
|
|
|
84
69
|
});
|
|
85
70
|
return;
|
|
86
71
|
}
|
|
87
|
-
// Check for timeout or abort signal properties
|
|
88
72
|
const hasTimeout = optionsArg.properties.some(prop => {
|
|
89
73
|
if (prop.type !== 'Property')
|
|
90
74
|
return false;
|
|
@@ -1,17 +1,6 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.requireToolConfirmation = void 0;
|
|
9
|
-
/**
|
|
10
|
-
* @fileoverview Require human confirmation for destructive tool operations
|
|
11
|
-
* @description Ensures tools that perform destructive actions have confirmation
|
|
12
|
-
* @see OWASP ASI09: Human-Agent Trust Exploitation
|
|
13
|
-
* @see OWASP LLM06: Excessive Agency
|
|
14
|
-
*/
|
|
15
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
16
5
|
exports.requireToolConfirmation = (0, eslint_devkit_1.createRule)({
|
|
17
6
|
name: 'require-tool-confirmation',
|
|
@@ -67,9 +56,6 @@ exports.requireToolConfirmation = (0, eslint_devkit_1.createRule)({
|
|
|
67
56
|
const destructivePatterns = options.destructivePatterns ?? [
|
|
68
57
|
'delete', 'remove', 'transfer', 'execute', 'update', 'create',
|
|
69
58
|
];
|
|
70
|
-
/**
|
|
71
|
-
* Check if a tool name suggests destructive operation
|
|
72
|
-
*/
|
|
73
59
|
function isDestructiveTool(name) {
|
|
74
60
|
const lowerName = name.toLowerCase();
|
|
75
61
|
for (const pattern of destructivePatterns) {
|
|
@@ -79,15 +65,11 @@ exports.requireToolConfirmation = (0, eslint_devkit_1.createRule)({
|
|
|
79
65
|
}
|
|
80
66
|
return null;
|
|
81
67
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Check if tool has confirmation requirement
|
|
84
|
-
*/
|
|
85
|
-
// oxlint-disable-next-line consistent-function-scoping
|
|
86
68
|
function hasConfirmationFlag(toolDef) {
|
|
87
69
|
const confirmationProps = new Set([
|
|
88
70
|
'requiresConfirmation', 'requireConfirmation', 'confirmation',
|
|
89
71
|
'requiresApproval', 'requireApproval', 'approval',
|
|
90
|
-
'dangerouslyAllowBrowser',
|
|
72
|
+
'dangerouslyAllowBrowser',
|
|
91
73
|
]);
|
|
92
74
|
return toolDef.properties.some(prop => {
|
|
93
75
|
if (prop.type !== 'Property')
|
|
@@ -98,21 +80,17 @@ exports.requireToolConfirmation = (0, eslint_devkit_1.createRule)({
|
|
|
98
80
|
}
|
|
99
81
|
return {
|
|
100
82
|
Property(node) {
|
|
101
|
-
// Looking for tool definitions in tools object
|
|
102
83
|
if (node.key.type !== 'Identifier' && node.key.type !== 'Literal')
|
|
103
84
|
return;
|
|
104
85
|
const toolName = node.key.type === 'Identifier'
|
|
105
86
|
? node.key.name
|
|
106
87
|
: String(node.key.value);
|
|
107
|
-
// Check if this looks like a tool definition
|
|
108
88
|
if (node.value.type !== 'ObjectExpression' && node.value.type !== 'CallExpression') {
|
|
109
89
|
return;
|
|
110
90
|
}
|
|
111
|
-
// Check if tool name suggests destructive operation
|
|
112
91
|
const destructiveOp = isDestructiveTool(toolName);
|
|
113
92
|
if (!destructiveOp)
|
|
114
93
|
return;
|
|
115
|
-
// Check the parent to ensure this is in a tools object
|
|
116
94
|
const parent = node.parent;
|
|
117
95
|
if (parent?.type !== 'ObjectExpression')
|
|
118
96
|
return;
|
|
@@ -122,7 +100,6 @@ exports.requireToolConfirmation = (0, eslint_devkit_1.createRule)({
|
|
|
122
100
|
const toolsKey = grandparent.key;
|
|
123
101
|
if (toolsKey.type !== 'Identifier' || toolsKey.name !== 'tools')
|
|
124
102
|
return;
|
|
125
|
-
// For object expressions, check for confirmation flag
|
|
126
103
|
if (node.value.type === 'ObjectExpression') {
|
|
127
104
|
if (!hasConfirmationFlag(node.value)) {
|
|
128
105
|
context.report({
|
|
@@ -135,7 +112,6 @@ exports.requireToolConfirmation = (0, eslint_devkit_1.createRule)({
|
|
|
135
112
|
});
|
|
136
113
|
}
|
|
137
114
|
}
|
|
138
|
-
// For CallExpressions (tool() helper), we'll assume it might be handled
|
|
139
115
|
},
|
|
140
116
|
};
|
|
141
117
|
},
|
|
@@ -1,17 +1,6 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.requireToolSchema = void 0;
|
|
9
|
-
/**
|
|
10
|
-
* @fileoverview Require Zod schema validation for tool parameters
|
|
11
|
-
* @description Ensures all tools have proper inputSchema defined with Zod
|
|
12
|
-
* @see https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling
|
|
13
|
-
* @see OWASP ASI02: Tool Misuse & Exploitation
|
|
14
|
-
*/
|
|
15
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
16
5
|
exports.requireToolSchema = (0, eslint_devkit_1.createRule)({
|
|
17
6
|
name: 'require-tool-schema',
|
|
@@ -70,20 +59,16 @@ exports.requireToolSchema = (0, eslint_devkit_1.createRule)({
|
|
|
70
59
|
const allowInTests = options.allowInTests ?? false;
|
|
71
60
|
const sourceCode = context.sourceCode;
|
|
72
61
|
const filename = context.filename;
|
|
73
|
-
// Skip test files if allowed
|
|
74
62
|
if (allowInTests && /\.(test|spec)\.[jt]sx?$/.test(filename)) {
|
|
75
63
|
return {};
|
|
76
64
|
}
|
|
77
|
-
// Vercel AI SDK functions that use tools
|
|
78
65
|
const functionsWithTools = ['generateText', 'streamText', 'generateObject', 'streamObject'];
|
|
79
66
|
return {
|
|
80
67
|
CallExpression(node) {
|
|
81
68
|
const callee = sourceCode.getText(node.callee);
|
|
82
|
-
// Check if this is a target AI function or a tool() helper
|
|
83
69
|
const isAIFunction = functionsWithTools.some(fn => callee.includes(fn));
|
|
84
70
|
const isToolHelper = callee === 'tool' || callee.endsWith('.tool');
|
|
85
71
|
if (isToolHelper) {
|
|
86
|
-
// Check tool() helper for inputSchema
|
|
87
72
|
const toolArg = node.arguments[0];
|
|
88
73
|
if (toolArg && toolArg.type === 'ObjectExpression') {
|
|
89
74
|
const hasInputSchema = toolArg.properties.some(prop => {
|
|
@@ -104,7 +89,6 @@ exports.requireToolSchema = (0, eslint_devkit_1.createRule)({
|
|
|
104
89
|
}
|
|
105
90
|
if (!isAIFunction)
|
|
106
91
|
return;
|
|
107
|
-
// Check for tools property in AI function call
|
|
108
92
|
const optionsArg = node.arguments[0];
|
|
109
93
|
if (!optionsArg || optionsArg.type !== 'ObjectExpression')
|
|
110
94
|
return;
|
|
@@ -116,7 +100,6 @@ exports.requireToolSchema = (0, eslint_devkit_1.createRule)({
|
|
|
116
100
|
});
|
|
117
101
|
if (!toolsProp)
|
|
118
102
|
return;
|
|
119
|
-
// Check each tool in the tools object
|
|
120
103
|
if (toolsProp.value.type === 'ObjectExpression') {
|
|
121
104
|
for (const toolDef of toolsProp.value.properties) {
|
|
122
105
|
if (toolDef.type !== 'Property')
|
|
@@ -126,7 +109,6 @@ exports.requireToolSchema = (0, eslint_devkit_1.createRule)({
|
|
|
126
109
|
: toolDef.key.type === 'Literal'
|
|
127
110
|
? String(toolDef.key.value)
|
|
128
111
|
: 'unknown';
|
|
129
|
-
// Check if tool value is an object with inputSchema
|
|
130
112
|
if (toolDef.value.type === 'ObjectExpression') {
|
|
131
113
|
const hasInputSchema = toolDef.value.properties.some(prop => {
|
|
132
114
|
if (prop.type !== 'Property')
|
|
@@ -142,7 +124,6 @@ exports.requireToolSchema = (0, eslint_devkit_1.createRule)({
|
|
|
142
124
|
});
|
|
143
125
|
}
|
|
144
126
|
}
|
|
145
|
-
// If it's a CallExpression (tool() helper), it will be checked separately
|
|
146
127
|
}
|
|
147
128
|
}
|
|
148
129
|
},
|
|
@@ -1,17 +1,6 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.requireValidatedPrompt = void 0;
|
|
9
|
-
/**
|
|
10
|
-
* @fileoverview Require validated prompts in generateText calls
|
|
11
|
-
* @description Detects when user input is passed directly to generateText without validation
|
|
12
|
-
* @see https://sdk.vercel.ai/docs/ai-sdk-core/generating-text
|
|
13
|
-
* @see https://owasp.org/www-project-top-10-for-large-language-model-applications/
|
|
14
|
-
*/
|
|
15
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
16
5
|
const prompt_props_1 = require("../../utils/prompt-props");
|
|
17
6
|
exports.requireValidatedPrompt = (0, eslint_devkit_1.createRule)({
|
|
@@ -109,42 +98,29 @@ exports.requireValidatedPrompt = (0, eslint_devkit_1.createRule)({
|
|
|
109
98
|
const allowInTests = options.allowInTests ?? false;
|
|
110
99
|
const sourceCode = context.sourceCode;
|
|
111
100
|
const filename = context.filename;
|
|
112
|
-
// Skip test files if allowed
|
|
113
101
|
if (allowInTests && /\.(test|spec)\.[jt]sx?$/.test(filename)) {
|
|
114
102
|
return {};
|
|
115
103
|
}
|
|
116
|
-
// Vercel AI SDK function names
|
|
117
104
|
const vercelAIFunctions = [
|
|
118
105
|
'generateText',
|
|
119
106
|
'streamText',
|
|
120
107
|
'generateObject',
|
|
121
108
|
'streamObject',
|
|
122
109
|
];
|
|
123
|
-
/**
|
|
124
|
-
* Check if a node is a call to a validation function
|
|
125
|
-
*/
|
|
126
110
|
function isValidatedCall(node) {
|
|
127
111
|
if (node.type !== 'CallExpression')
|
|
128
112
|
return false;
|
|
129
113
|
const callee = sourceCode.getText(node.callee);
|
|
130
114
|
return validatorFunctions.some((v) => callee.includes(v));
|
|
131
115
|
}
|
|
132
|
-
/**
|
|
133
|
-
* Check if an identifier suggests user input
|
|
134
|
-
*/
|
|
135
116
|
function isUserInput(node) {
|
|
136
117
|
const text = sourceCode.getText(node);
|
|
137
118
|
return userInputPatterns.some((pattern) => text.includes(pattern));
|
|
138
119
|
}
|
|
139
|
-
/**
|
|
140
|
-
* Check if a node contains unsafe user input
|
|
141
|
-
*/
|
|
142
120
|
function hasUnsafeUserInput(node) {
|
|
143
|
-
// If it's validated, it's safe
|
|
144
121
|
if (isValidatedCall(node)) {
|
|
145
122
|
return { unsafe: false };
|
|
146
123
|
}
|
|
147
|
-
// Check for template literals with user input
|
|
148
124
|
if (node.type === 'TemplateLiteral') {
|
|
149
125
|
for (const expr of node.expressions) {
|
|
150
126
|
const result = hasUnsafeUserInput(expr);
|
|
@@ -152,20 +128,17 @@ exports.requireValidatedPrompt = (0, eslint_devkit_1.createRule)({
|
|
|
152
128
|
return result;
|
|
153
129
|
}
|
|
154
130
|
}
|
|
155
|
-
// Check identifiers
|
|
156
131
|
if (node.type === 'Identifier') {
|
|
157
132
|
if (isUserInput(node)) {
|
|
158
133
|
return { unsafe: true, input: node.name };
|
|
159
134
|
}
|
|
160
135
|
}
|
|
161
|
-
// Check member expressions
|
|
162
136
|
if (node.type === 'MemberExpression') {
|
|
163
137
|
const text = sourceCode.getText(node);
|
|
164
138
|
if (isUserInput(node)) {
|
|
165
139
|
return { unsafe: true, input: text };
|
|
166
140
|
}
|
|
167
141
|
}
|
|
168
|
-
// Check binary expressions (concatenation)
|
|
169
142
|
if (node.type === 'BinaryExpression') {
|
|
170
143
|
const leftResult = hasUnsafeUserInput(node.left);
|
|
171
144
|
if (leftResult.unsafe)
|
|
@@ -176,26 +149,20 @@ exports.requireValidatedPrompt = (0, eslint_devkit_1.createRule)({
|
|
|
176
149
|
}
|
|
177
150
|
return { unsafe: false };
|
|
178
151
|
}
|
|
179
|
-
/**
|
|
180
|
-
* Check generateText/streamText call for unsafe prompts
|
|
181
|
-
*/
|
|
182
152
|
function checkAICall(node) {
|
|
183
153
|
const callee = sourceCode.getText(node.callee);
|
|
184
|
-
// Get which function is being called
|
|
185
154
|
const matchedFunction = vercelAIFunctions.find(fn => callee.includes(fn));
|
|
186
155
|
if (!matchedFunction) {
|
|
187
156
|
return;
|
|
188
157
|
}
|
|
189
|
-
// Check first argument (options object)
|
|
190
158
|
const optionsArg = node.arguments[0];
|
|
191
159
|
if (!optionsArg || optionsArg.type !== 'ObjectExpression') {
|
|
192
160
|
return;
|
|
193
161
|
}
|
|
194
|
-
// Check for unsafe prompt property
|
|
195
162
|
for (const prop of optionsArg.properties) {
|
|
196
163
|
if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
|
|
197
164
|
continue;
|
|
198
|
-
const keyName = (0, prompt_props_1.getStaticPropName)(prop
|
|
165
|
+
const keyName = (0, prompt_props_1.getStaticPropName)(prop);
|
|
199
166
|
if (keyName === 'prompt') {
|
|
200
167
|
const result = hasUnsafeUserInput(prop.value);
|
|
201
168
|
if (result.unsafe) {
|
package/src/types/index.js
CHANGED
|
@@ -1,40 +1,20 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.SYSTEM_PROMPT_PROPS = void 0;
|
|
9
4
|
exports.isSystemPromptProp = isSystemPromptProp;
|
|
10
5
|
exports.getStaticPropName = getStaticPropName;
|
|
11
6
|
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
7
|
exports.SYSTEM_PROMPT_PROPS = new Set(['instructions', 'system']);
|
|
21
|
-
/** True when `name` is a property the AI SDK treats as the system prompt. */
|
|
22
8
|
function isSystemPromptProp(name) {
|
|
23
9
|
return name !== null && exports.SYSTEM_PROMPT_PROPS.has(name);
|
|
24
10
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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;
|
|
11
|
+
function getStaticPropName(prop) {
|
|
12
|
+
if (prop.computed)
|
|
13
|
+
return null;
|
|
14
|
+
if (prop.key.type === eslint_devkit_1.AST_NODE_TYPES.Identifier)
|
|
15
|
+
return prop.key.name;
|
|
16
|
+
if (prop.key.type === eslint_devkit_1.AST_NODE_TYPES.Literal && typeof prop.key.value === 'string') {
|
|
17
|
+
return prop.key.value;
|
|
18
|
+
}
|
|
39
19
|
return null;
|
|
40
20
|
}
|
package/AGENTS.md
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
# AGENTS.md
|
|
2
|
-
|
|
3
|
-
> Context for AI coding agents working on eslint-plugin-vercel-ai-security
|
|
4
|
-
|
|
5
|
-
## Setup Commands
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
# Install dependencies (from monorepo root)
|
|
9
|
-
npm install
|
|
10
|
-
|
|
11
|
-
# Build this package
|
|
12
|
-
nx build eslint-plugin-vercel-ai-security
|
|
13
|
-
|
|
14
|
-
# Run tests
|
|
15
|
-
nx test eslint-plugin-vercel-ai-security
|
|
16
|
-
|
|
17
|
-
# Run tests with coverage
|
|
18
|
-
nx test eslint-plugin-vercel-ai-security --coverage
|
|
19
|
-
|
|
20
|
-
# Lint this package
|
|
21
|
-
nx lint eslint-plugin-vercel-ai-security
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Code Style
|
|
25
|
-
|
|
26
|
-
- TypeScript strict mode with `@interlace/eslint-devkit` types
|
|
27
|
-
- Use `AST_NODE_TYPES` constants, never string literals for node types
|
|
28
|
-
- Use `formatLLMMessage()` for all rule error messages
|
|
29
|
-
- Include CWE, CVSS, OWASP LLM Top 10 in every security message
|
|
30
|
-
- Use `c8 ignore` comments with documented reasons for untestable code
|
|
31
|
-
- Single-pass AST traversal patterns (O(n) complexity)
|
|
32
|
-
|
|
33
|
-
## Testing Instructions
|
|
34
|
-
|
|
35
|
-
- Tests use `@typescript-eslint/rule-tester` with Vitest
|
|
36
|
-
- Each rule has `index.ts` (implementation) and `*.test.ts` (tests) in same directory
|
|
37
|
-
- Run specific rule test: `nx test eslint-plugin-vercel-ai-security --testPathPattern="require-validated-prompt"`
|
|
38
|
-
- Coverage target: ≥90% lines, ≥95% functions
|
|
39
|
-
- All tests must pass before committing
|
|
40
|
-
|
|
41
|
-
## Project Structure
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
src/
|
|
45
|
-
├── index.ts # Plugin entry, exports rules and configs
|
|
46
|
-
└── rules/ # Rule directories
|
|
47
|
-
└── [rule-name]/
|
|
48
|
-
├── index.ts # Rule implementation
|
|
49
|
-
└── *.test.ts # Rule tests
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## Plugin Purpose
|
|
53
|
-
|
|
54
|
-
Security rules for **Vercel AI SDK** (`ai` package). Covers prompt injection, sensitive data exposure, API key security, output handling, tool security, and resource limits.
|
|
55
|
-
|
|
56
|
-
## Target SDK Functions
|
|
57
|
-
|
|
58
|
-
```typescript
|
|
59
|
-
import {
|
|
60
|
-
generateText,
|
|
61
|
-
streamText,
|
|
62
|
-
generateObject,
|
|
63
|
-
streamObject,
|
|
64
|
-
tool,
|
|
65
|
-
} from 'ai';
|
|
66
|
-
import { createOpenAI, createAnthropic, createGoogle } from '@ai-sdk/*';
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Quick Reference: All 10 Rules
|
|
70
|
-
|
|
71
|
-
| Rule | Severity | Fix Pattern |
|
|
72
|
-
| --------------------------- | -------- | ------------------------------------ |
|
|
73
|
-
| `require-validated-prompt` | CRITICAL | `prompt: validateInput(userInput)` |
|
|
74
|
-
| `no-sensitive-in-prompt` | CRITICAL | Remove password/token from prompt |
|
|
75
|
-
| `no-hardcoded-api-keys` | CRITICAL | `apiKey: process.env.OPENAI_API_KEY` |
|
|
76
|
-
| `no-unsafe-output-handling` | CRITICAL | Don't pass `result.text` to `eval()` |
|
|
77
|
-
| `require-tool-schema` | HIGH | Add `inputSchema: z.object({...})` |
|
|
78
|
-
| `require-max-tokens` | HIGH | Add `maxTokens: 4096` |
|
|
79
|
-
| `require-max-steps` | HIGH | Add `maxSteps: 5` when using tools |
|
|
80
|
-
| `require-tool-confirmation` | HIGH | Add `requiresConfirmation: true` |
|
|
81
|
-
| `require-error-handling` | MEDIUM | Wrap in `try { } catch { }` |
|
|
82
|
-
| `require-abort-signal` | LOW | Add `abortSignal: controller.signal` |
|
|
83
|
-
|
|
84
|
-
## Common Fix Patterns
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
// Prompt Injection (CWE-74)
|
|
88
|
-
// BAD: await generateText({ prompt: userInput })
|
|
89
|
-
// GOOD: await generateText({ prompt: validateInput(userInput) })
|
|
90
|
-
|
|
91
|
-
// API Keys (CWE-798)
|
|
92
|
-
// BAD: createOpenAI({ apiKey: 'sk-proj-abc123...' })
|
|
93
|
-
// GOOD: createOpenAI({ apiKey: process.env.OPENAI_API_KEY })
|
|
94
|
-
|
|
95
|
-
// Tool Schema (CWE-20)
|
|
96
|
-
// BAD: tool({ execute: async (params) => search(params) })
|
|
97
|
-
// GOOD: tool({ inputSchema: z.object({ query: z.string().max(500) }), execute: ... })
|
|
98
|
-
|
|
99
|
-
// Max Tokens (CWE-770)
|
|
100
|
-
// BAD: await generateText({ prompt: 'Write a story' })
|
|
101
|
-
// GOOD: await generateText({ prompt: 'Write a story', maxTokens: 4096 })
|
|
102
|
-
|
|
103
|
-
// Tool Confirmation (CWE-862)
|
|
104
|
-
// BAD: tools: { deleteFile: { execute: async (p) => fs.unlink(p.path) } }
|
|
105
|
-
// GOOD: tools: { deleteFile: { requiresConfirmation: true, execute: ... } }
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## OWASP Reference
|
|
109
|
-
|
|
110
|
-
| OWASP LLM Top 10 2025 | Rule |
|
|
111
|
-
| ----------------------- | ----------------------------------------- |
|
|
112
|
-
| LLM01: Prompt Injection | `require-validated-prompt` |
|
|
113
|
-
| LLM02: Sensitive Info | `no-sensitive-in-prompt` |
|
|
114
|
-
| LLM05: Output Handling | `no-unsafe-output-handling` |
|
|
115
|
-
| LLM06: Excessive Agency | `require-tool-confirmation` |
|
|
116
|
-
| LLM10: Unbounded | `require-max-tokens`, `require-max-steps` |
|
|
117
|
-
|
|
118
|
-
| OWASP Agentic Top 10 2026 | Rule |
|
|
119
|
-
| ------------------------- | --------------------------- |
|
|
120
|
-
| ASI02: Tool Misuse | `require-tool-schema` |
|
|
121
|
-
| ASI03: Identity Abuse | `no-hardcoded-api-keys` |
|
|
122
|
-
| ASI05: Code Execution | `no-unsafe-output-handling` |
|
|
123
|
-
| ASI08: Cascading Failures | `require-error-handling` |
|
|
124
|
-
|
|
125
|
-
## Suppression Patterns
|
|
126
|
-
|
|
127
|
-
```typescript
|
|
128
|
-
// Suppress single line
|
|
129
|
-
// eslint-disable-next-line vercel-ai-security/require-validated-prompt -- Trusted internal input
|
|
130
|
-
await generateText({ prompt: internalPrompt });
|
|
131
|
-
```
|