eslint-plugin-vercel-ai-security 1.4.2 → 1.5.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-vercel-ai-security",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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",
|
|
@@ -74,7 +74,8 @@
|
|
|
74
74
|
"node": ">=18.0.0"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"eslint": "^8.0.0 || ^9.0.0 || ^10.0.0"
|
|
77
|
+
"eslint": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
|
78
|
+
"ai": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
|
78
79
|
},
|
|
79
80
|
"peerDependenciesMeta": {
|
|
80
81
|
"ai": {
|
|
@@ -83,7 +84,7 @@
|
|
|
83
84
|
},
|
|
84
85
|
"dependencies": {
|
|
85
86
|
"tslib": "^2.3.0",
|
|
86
|
-
"@interlace/eslint-devkit": "^1.
|
|
87
|
+
"@interlace/eslint-devkit": "^1.7.0"
|
|
87
88
|
},
|
|
88
89
|
"funding": {
|
|
89
90
|
"type": "github",
|
package/src/index.js
CHANGED
|
@@ -91,7 +91,16 @@ exports.noUnsafeOutputHandling = (0, eslint_devkit_1.createRule)({
|
|
|
91
91
|
const sourceCode = context.sourceCode;
|
|
92
92
|
const dangerousFunctions = ['eval', 'Function', 'execSync', 'exec', 'spawn', 'execFile'];
|
|
93
93
|
const sqlPatterns = ['query', 'execute', 'run', 'raw'];
|
|
94
|
-
const
|
|
94
|
+
const aiBoundVariables = new Set();
|
|
95
|
+
function isAIBound(node) {
|
|
96
|
+
if (node.type !== 'Identifier')
|
|
97
|
+
return false;
|
|
98
|
+
for (const variable of aiBoundVariables) {
|
|
99
|
+
if (variable.references.some((r) => r.identifier === node))
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
95
104
|
const AI_SDK_CALLS = new Set(['generateText', 'streamText', 'generateObject', 'streamObject']);
|
|
96
105
|
function isAISDKCall(node) {
|
|
97
106
|
let target = node;
|
|
@@ -108,29 +117,31 @@ exports.noUnsafeOutputHandling = (0, eslint_devkit_1.createRule)({
|
|
|
108
117
|
return false;
|
|
109
118
|
}
|
|
110
119
|
function isLikelyAIOutput(node) {
|
|
111
|
-
if (node.type === 'MemberExpression' && node.object
|
|
112
|
-
aiBoundNames.has(node.object.name)) {
|
|
120
|
+
if (node.type === 'MemberExpression' && isAIBound(node.object)) {
|
|
113
121
|
return true;
|
|
114
122
|
}
|
|
115
|
-
if (
|
|
123
|
+
if (isAIBound(node)) {
|
|
116
124
|
return true;
|
|
117
125
|
}
|
|
118
126
|
const text = sourceCode.getText(node);
|
|
119
127
|
return aiOutputPatterns.some((pattern) => text.includes(pattern));
|
|
120
128
|
}
|
|
129
|
+
function containsAIOutput(node) {
|
|
130
|
+
if (node.type === 'TemplateLiteral') {
|
|
131
|
+
return node.expressions.some(containsAIOutput);
|
|
132
|
+
}
|
|
133
|
+
if (node.type === 'BinaryExpression') {
|
|
134
|
+
return (node.operator === '+' &&
|
|
135
|
+
(containsAIOutput(node.left) || containsAIOutput(node.right)));
|
|
136
|
+
}
|
|
137
|
+
return isLikelyAIOutput(node);
|
|
138
|
+
}
|
|
121
139
|
return {
|
|
122
140
|
VariableDeclarator(node) {
|
|
123
141
|
if (!node.init || !isAISDKCall(node.init))
|
|
124
142
|
return;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
else if (node.id.type === 'ObjectPattern') {
|
|
129
|
-
for (const prop of node.id.properties) {
|
|
130
|
-
if (prop.type === 'Property' && prop.value.type === 'Identifier') {
|
|
131
|
-
aiBoundNames.add(prop.value.name);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
143
|
+
for (const variable of sourceCode.getDeclaredVariables(node)) {
|
|
144
|
+
aiBoundVariables.add(variable);
|
|
134
145
|
}
|
|
135
146
|
},
|
|
136
147
|
CallExpression(node) {
|
|
@@ -154,7 +165,7 @@ exports.noUnsafeOutputHandling = (0, eslint_devkit_1.createRule)({
|
|
|
154
165
|
if (isSQLFunction) {
|
|
155
166
|
for (const arg of node.arguments) {
|
|
156
167
|
if (arg.type === 'TemplateLiteral' || arg.type === 'BinaryExpression') {
|
|
157
|
-
if (
|
|
168
|
+
if (containsAIOutput(arg)) {
|
|
158
169
|
context.report({
|
|
159
170
|
node: arg,
|
|
160
171
|
messageId: 'unsafeOutputInSQL',
|
|
@@ -76,7 +76,10 @@ exports.requireMaxTokens = (0, eslint_devkit_1.createRule)({
|
|
|
76
76
|
: prop.key.type === 'Literal'
|
|
77
77
|
? String(prop.key.value)
|
|
78
78
|
: null;
|
|
79
|
-
return keyName === 'maxTokens' ||
|
|
79
|
+
return (keyName === 'maxTokens' ||
|
|
80
|
+
keyName === 'max_tokens' ||
|
|
81
|
+
keyName === 'maxOutputTokens' ||
|
|
82
|
+
keyName === 'max_output_tokens');
|
|
80
83
|
});
|
|
81
84
|
if (!hasMaxTokens) {
|
|
82
85
|
context.report({
|