eslint-plugin-vercel-ai-security 1.5.2 → 1.5.3
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 +2 -2
- package/src/index.js +1 -145
- package/src/oxlint.js +1 -3
- package/src/rules/no-dynamic-system-prompt/index.js +1 -96
- package/src/rules/no-hardcoded-api-keys/index.js +1 -122
- package/src/rules/no-sensitive-in-prompt/index.js +1 -121
- package/src/rules/no-system-prompt-leak/index.js +1 -116
- package/src/rules/no-training-data-exposure/index.js +1 -96
- package/src/rules/no-unsafe-output-handling/index.js +1 -193
- package/src/rules/require-abort-signal/index.js +1 -80
- package/src/rules/require-audit-logging/index.js +1 -110
- package/src/rules/require-embedding-validation/index.js +1 -119
- package/src/rules/require-error-handling/index.js +1 -92
- package/src/rules/require-max-steps/index.js +1 -88
- package/src/rules/require-max-tokens/index.js +1 -94
- package/src/rules/require-output-filtering/index.js +1 -123
- package/src/rules/require-output-validation/index.js +1 -121
- package/src/rules/require-rag-content-validation/index.js +1 -150
- package/src/rules/require-request-timeout/index.js +1 -88
- package/src/rules/require-tool-confirmation/index.js +1 -118
- package/src/rules/require-tool-schema/index.js +1 -132
- package/src/rules/require-validated-prompt/index.js +1 -194
- package/src/types/index.js +1 -2
- package/src/utils/prompt-props.js +1 -20
|
@@ -1,194 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.requireValidatedPrompt = void 0;
|
|
4
|
-
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
5
|
-
const prompt_props_1 = require("../../utils/prompt-props");
|
|
6
|
-
exports.requireValidatedPrompt = (0, eslint_devkit_1.createRule)({
|
|
7
|
-
name: 'require-validated-prompt',
|
|
8
|
-
meta: {
|
|
9
|
-
type: 'problem',
|
|
10
|
-
docs: {
|
|
11
|
-
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-validated-prompt.md',
|
|
12
|
-
description: 'Require validated/sanitized prompts in generateText and streamText calls',
|
|
13
|
-
cwe: 'CWE-74',
|
|
14
|
-
cvss: 9,
|
|
15
|
-
},
|
|
16
|
-
messages: {
|
|
17
|
-
unsafePrompt: (0, eslint_devkit_1.formatLLMMessage)({
|
|
18
|
-
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
19
|
-
issueName: 'Unsafe Prompt in Vercel AI SDK',
|
|
20
|
-
cwe: 'CWE-74',
|
|
21
|
-
owasp: 'A03:2021',
|
|
22
|
-
cvss: 9.0,
|
|
23
|
-
description: 'User input "{{input}}" passed directly to {{function}} prompt without validation',
|
|
24
|
-
severity: 'CRITICAL',
|
|
25
|
-
compliance: ['SOC2', 'GDPR'],
|
|
26
|
-
fix: 'Validate input before use: generateText({ prompt: validateInput(userInput) })',
|
|
27
|
-
documentationLink: 'https://owasp.org/www-project-top-10-for-large-language-model-applications/',
|
|
28
|
-
}),
|
|
29
|
-
unsafeSystemPrompt: (0, eslint_devkit_1.formatLLMMessage)({
|
|
30
|
-
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
31
|
-
issueName: 'Unsafe System Prompt',
|
|
32
|
-
cwe: 'CWE-74',
|
|
33
|
-
owasp: 'A03:2021',
|
|
34
|
-
cvss: 8.5,
|
|
35
|
-
description: 'Dynamic value in system prompt can lead to prompt injection',
|
|
36
|
-
severity: 'HIGH',
|
|
37
|
-
fix: 'Use static system prompts or validate dynamic content',
|
|
38
|
-
documentationLink: 'https://owasp.org/www-project-top-10-for-large-language-model-applications/',
|
|
39
|
-
}),
|
|
40
|
-
},
|
|
41
|
-
schema: [
|
|
42
|
-
{
|
|
43
|
-
type: 'object',
|
|
44
|
-
properties: {
|
|
45
|
-
validatorFunctions: {
|
|
46
|
-
type: 'array',
|
|
47
|
-
items: { type: 'string' },
|
|
48
|
-
description: 'Function names considered as input validators',
|
|
49
|
-
},
|
|
50
|
-
userInputPatterns: {
|
|
51
|
-
type: 'array',
|
|
52
|
-
items: { type: 'string' },
|
|
53
|
-
description: 'Variable patterns that suggest user input (regex)',
|
|
54
|
-
},
|
|
55
|
-
allowInTests: {
|
|
56
|
-
type: 'boolean',
|
|
57
|
-
description: 'Allow unsafe patterns in test files',
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
additionalProperties: false,
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
defaultOptions: [
|
|
65
|
-
{
|
|
66
|
-
validatorFunctions: [
|
|
67
|
-
'validateInput',
|
|
68
|
-
'sanitizeInput',
|
|
69
|
-
'validatePrompt',
|
|
70
|
-
'sanitizePrompt',
|
|
71
|
-
'escapeInput',
|
|
72
|
-
'cleanInput',
|
|
73
|
-
],
|
|
74
|
-
userInputPatterns: [
|
|
75
|
-
'userInput',
|
|
76
|
-
'userPrompt',
|
|
77
|
-
'userMessage',
|
|
78
|
-
'userQuery',
|
|
79
|
-
'userContent',
|
|
80
|
-
'input',
|
|
81
|
-
'query',
|
|
82
|
-
'message',
|
|
83
|
-
'req.body',
|
|
84
|
-
'request.body',
|
|
85
|
-
],
|
|
86
|
-
allowInTests: false,
|
|
87
|
-
},
|
|
88
|
-
],
|
|
89
|
-
create(context) {
|
|
90
|
-
const [options = {}] = context.options;
|
|
91
|
-
const validatorFunctions = options.validatorFunctions ?? [
|
|
92
|
-
'validateInput', 'sanitizeInput', 'validatePrompt', 'sanitizePrompt',
|
|
93
|
-
];
|
|
94
|
-
const userInputPatterns = options.userInputPatterns ?? [
|
|
95
|
-
'userInput', 'userPrompt', 'userMessage', 'userQuery', 'userContent',
|
|
96
|
-
'input', 'query', 'message', 'req.body', 'request.body',
|
|
97
|
-
];
|
|
98
|
-
const allowInTests = options.allowInTests ?? false;
|
|
99
|
-
const sourceCode = context.sourceCode;
|
|
100
|
-
const filename = context.filename;
|
|
101
|
-
if (allowInTests && /\.(test|spec)\.[jt]sx?$/.test(filename)) {
|
|
102
|
-
return {};
|
|
103
|
-
}
|
|
104
|
-
const vercelAIFunctions = [
|
|
105
|
-
'generateText',
|
|
106
|
-
'streamText',
|
|
107
|
-
'generateObject',
|
|
108
|
-
'streamObject',
|
|
109
|
-
];
|
|
110
|
-
function isValidatedCall(node) {
|
|
111
|
-
if (node.type !== 'CallExpression')
|
|
112
|
-
return false;
|
|
113
|
-
const callee = sourceCode.getText(node.callee);
|
|
114
|
-
return validatorFunctions.some((v) => callee.includes(v));
|
|
115
|
-
}
|
|
116
|
-
function isUserInput(node) {
|
|
117
|
-
const text = sourceCode.getText(node);
|
|
118
|
-
return userInputPatterns.some((pattern) => text.includes(pattern));
|
|
119
|
-
}
|
|
120
|
-
function hasUnsafeUserInput(node) {
|
|
121
|
-
if (isValidatedCall(node)) {
|
|
122
|
-
return { unsafe: false };
|
|
123
|
-
}
|
|
124
|
-
if (node.type === 'TemplateLiteral') {
|
|
125
|
-
for (const expr of node.expressions) {
|
|
126
|
-
const result = hasUnsafeUserInput(expr);
|
|
127
|
-
if (result.unsafe)
|
|
128
|
-
return result;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (node.type === 'Identifier') {
|
|
132
|
-
if (isUserInput(node)) {
|
|
133
|
-
return { unsafe: true, input: node.name };
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if (node.type === 'MemberExpression') {
|
|
137
|
-
const text = sourceCode.getText(node);
|
|
138
|
-
if (isUserInput(node)) {
|
|
139
|
-
return { unsafe: true, input: text };
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (node.type === 'BinaryExpression') {
|
|
143
|
-
const leftResult = hasUnsafeUserInput(node.left);
|
|
144
|
-
if (leftResult.unsafe)
|
|
145
|
-
return leftResult;
|
|
146
|
-
const rightResult = hasUnsafeUserInput(node.right);
|
|
147
|
-
if (rightResult.unsafe)
|
|
148
|
-
return rightResult;
|
|
149
|
-
}
|
|
150
|
-
return { unsafe: false };
|
|
151
|
-
}
|
|
152
|
-
function checkAICall(node) {
|
|
153
|
-
const callee = sourceCode.getText(node.callee);
|
|
154
|
-
const matchedFunction = vercelAIFunctions.find(fn => callee.includes(fn));
|
|
155
|
-
if (!matchedFunction) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
const optionsArg = node.arguments[0];
|
|
159
|
-
if (!optionsArg || optionsArg.type !== 'ObjectExpression') {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
for (const prop of optionsArg.properties) {
|
|
163
|
-
if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
|
|
164
|
-
continue;
|
|
165
|
-
const keyName = (0, prompt_props_1.getStaticPropName)(prop);
|
|
166
|
-
if (keyName === 'prompt') {
|
|
167
|
-
const result = hasUnsafeUserInput(prop.value);
|
|
168
|
-
if (result.unsafe) {
|
|
169
|
-
context.report({
|
|
170
|
-
node: prop.value,
|
|
171
|
-
messageId: 'unsafePrompt',
|
|
172
|
-
data: {
|
|
173
|
-
input: result.input || 'user input',
|
|
174
|
-
function: matchedFunction,
|
|
175
|
-
},
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if ((0, prompt_props_1.isSystemPromptProp)(keyName)) {
|
|
180
|
-
const result = hasUnsafeUserInput(prop.value);
|
|
181
|
-
if (result.unsafe) {
|
|
182
|
-
context.report({
|
|
183
|
-
node: prop.value,
|
|
184
|
-
messageId: 'unsafeSystemPrompt',
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return {
|
|
191
|
-
CallExpression: checkAICall,
|
|
192
|
-
};
|
|
193
|
-
},
|
|
194
|
-
});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireValidatedPrompt=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const prompt_props_1=require("../../utils/prompt-props");exports.requireValidatedPrompt=(0,eslint_devkit_1.createRule)({name:"require-validated-prompt",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-validated-prompt.md",description:"Require validated/sanitized prompts in generateText and streamText calls",cwe:"CWE-74",cvss:9},messages:{unsafePrompt:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe Prompt in Vercel AI SDK",cwe:"CWE-74",owasp:"A03:2021",cvss:9,description:'User input "{{input}}" passed directly to {{function}} prompt without validation',severity:"CRITICAL",compliance:["SOC2","GDPR"],fix:"Validate input before use: generateText({ prompt: validateInput(userInput) })",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"}),unsafeSystemPrompt:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe System Prompt",cwe:"CWE-74",owasp:"A03:2021",cvss:8.5,description:"Dynamic value in system prompt can lead to prompt injection",severity:"HIGH",fix:"Use static system prompts or validate dynamic content",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"})},schema:[{type:"object",properties:{validatorFunctions:{type:"array",items:{type:"string"},description:"Function names considered as input validators"},userInputPatterns:{type:"array",items:{type:"string"},description:"Variable patterns that suggest user input (regex)"},allowInTests:{type:"boolean",description:"Allow unsafe patterns in test files"}},additionalProperties:false}]},defaultOptions:[{validatorFunctions:["validateInput","sanitizeInput","validatePrompt","sanitizePrompt","escapeInput","cleanInput"],userInputPatterns:["userInput","userPrompt","userMessage","userQuery","userContent","input","query","message","req.body","request.body"],allowInTests:false}],create(context){const[options={}]=context.options;const validatorFunctions=options.validatorFunctions??["validateInput","sanitizeInput","validatePrompt","sanitizePrompt"];const userInputPatterns=options.userInputPatterns??["userInput","userPrompt","userMessage","userQuery","userContent","input","query","message","req.body","request.body"];const allowInTests=options.allowInTests??false;const sourceCode=context.sourceCode;const filename=context.filename;if(allowInTests&&/\.(test|spec)\.[jt]sx?$/.test(filename)){return{}}const vercelAIFunctions=["generateText","streamText","generateObject","streamObject"];function isValidatedCall(node){if(node.type!=="CallExpression")return false;const callee=sourceCode.getText(node.callee);return validatorFunctions.some(v=>callee.includes(v))}function isUserInput(node){const text=sourceCode.getText(node);return userInputPatterns.some(pattern=>text.includes(pattern))}function hasUnsafeUserInput(node){if(isValidatedCall(node)){return{unsafe:false}}if(node.type==="TemplateLiteral"){for(const expr of node.expressions){const result=hasUnsafeUserInput(expr);if(result.unsafe)return result}}if(node.type==="Identifier"){if(isUserInput(node)){return{unsafe:true,input:node.name}}}if(node.type==="MemberExpression"){const text=sourceCode.getText(node);if(isUserInput(node)){return{unsafe:true,input:text}}}if(node.type==="BinaryExpression"){const leftResult=hasUnsafeUserInput(node.left);if(leftResult.unsafe)return leftResult;const rightResult=hasUnsafeUserInput(node.right);if(rightResult.unsafe)return rightResult}return{unsafe:false}}function checkAICall(node){const callee=sourceCode.getText(node.callee);const matchedFunction=vercelAIFunctions.find(fn=>callee.includes(fn));if(!matchedFunction){return}const optionsArg=node.arguments[0];if(!optionsArg||optionsArg.type!=="ObjectExpression"){return}for(const prop of optionsArg.properties){if(prop.type!==eslint_devkit_1.AST_NODE_TYPES.Property)continue;const keyName=(0,prompt_props_1.getStaticPropName)(prop);if(keyName==="prompt"){const result=hasUnsafeUserInput(prop.value);if(result.unsafe){context.report({node:prop.value,messageId:"unsafePrompt",data:{input:result.input||"user input",function:matchedFunction}})}}if((0,prompt_props_1.isSystemPromptProp)(keyName)){const result=hasUnsafeUserInput(prop.value);if(result.unsafe){context.report({node:prop.value,messageId:"unsafeSystemPrompt"})}}}}return{CallExpression:checkAICall}}});
|
package/src/types/index.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});
|
|
@@ -1,20 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SYSTEM_PROMPT_PROPS = void 0;
|
|
4
|
-
exports.isSystemPromptProp = isSystemPromptProp;
|
|
5
|
-
exports.getStaticPropName = getStaticPropName;
|
|
6
|
-
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
7
|
-
exports.SYSTEM_PROMPT_PROPS = new Set(['instructions', 'system']);
|
|
8
|
-
function isSystemPromptProp(name) {
|
|
9
|
-
return name !== null && exports.SYSTEM_PROMPT_PROPS.has(name);
|
|
10
|
-
}
|
|
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
|
-
}
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.SYSTEM_PROMPT_PROPS=void 0;exports.isSystemPromptProp=isSystemPromptProp;exports.getStaticPropName=getStaticPropName;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.SYSTEM_PROMPT_PROPS=new Set(["instructions","system"]);function isSystemPromptProp(name){return name!==null&&exports.SYSTEM_PROMPT_PROPS.has(name)}function getStaticPropName(prop){if(prop.computed)return null;if(prop.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return prop.key.name;if(prop.key.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof prop.key.value==="string"){return prop.key.value}return null}
|