eslint-plugin-vercel-ai-security 1.4.1 → 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.4.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.6.1"
87
+ "@interlace/eslint-devkit": "^1.7.0"
87
88
  },
88
89
  "funding": {
89
90
  "type": "github",
package/src/index.js CHANGED
@@ -82,7 +82,7 @@ exports.rules = {
82
82
  exports.plugin = {
83
83
  meta: {
84
84
  name: 'eslint-plugin-vercel-ai-security',
85
- version: '1.4.1',
85
+ version: '1.5.0',
86
86
  },
87
87
  rules: exports.rules,
88
88
  };
@@ -80,7 +80,7 @@ exports.noDynamicSystemPrompt = (0, eslint_devkit_1.createRule)({
80
80
  for (const prop of optionsArg.properties) {
81
81
  if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
82
82
  continue;
83
- const keyName = (0, prompt_props_1.getStaticPropName)(prop.key);
83
+ const keyName = (0, prompt_props_1.getStaticPropName)(prop);
84
84
  if (!(0, prompt_props_1.isSystemPromptProp)(keyName))
85
85
  continue;
86
86
  if (isDynamicContent(prop.value)) {
@@ -103,7 +103,7 @@ exports.noSensitiveInPrompt = (0, eslint_devkit_1.createRule)({
103
103
  for (const prop of optionsArg.properties) {
104
104
  if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
105
105
  continue;
106
- const keyName = (0, prompt_props_1.getStaticPropName)(prop.key);
106
+ const keyName = (0, prompt_props_1.getStaticPropName)(prop);
107
107
  if (!keyName || !propsToCheck.has(keyName))
108
108
  continue;
109
109
  const sensitiveVar = findSensitiveData(prop.value);
@@ -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 aiBoundNames = new Set();
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.type === 'Identifier' &&
112
- aiBoundNames.has(node.object.name)) {
120
+ if (node.type === 'MemberExpression' && isAIBound(node.object)) {
113
121
  return true;
114
122
  }
115
- if (node.type === 'Identifier' && aiBoundNames.has(node.name)) {
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
- if (node.id.type === 'Identifier') {
126
- aiBoundNames.add(node.id.name);
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 (isLikelyAIOutput(arg)) {
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' || keyName === 'max_tokens' || keyName === 'maxOutputTokens';
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({
@@ -132,7 +132,7 @@ exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
132
132
  for (const prop of optionsArg.properties) {
133
133
  if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
134
134
  continue;
135
- const keyName = (0, prompt_props_1.getStaticPropName)(prop.key);
135
+ const keyName = (0, prompt_props_1.getStaticPropName)(prop);
136
136
  if (keyName !== 'prompt' && !(0, prompt_props_1.isSystemPromptProp)(keyName))
137
137
  continue;
138
138
  const ragSource = containsRagContent(prop.value);
@@ -162,7 +162,7 @@ exports.requireValidatedPrompt = (0, eslint_devkit_1.createRule)({
162
162
  for (const prop of optionsArg.properties) {
163
163
  if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
164
164
  continue;
165
- const keyName = (0, prompt_props_1.getStaticPropName)(prop.key);
165
+ const keyName = (0, prompt_props_1.getStaticPropName)(prop);
166
166
  if (keyName === 'prompt') {
167
167
  const result = hasUnsafeUserInput(prop.value);
168
168
  if (result.unsafe) {
@@ -8,10 +8,13 @@ exports.SYSTEM_PROMPT_PROPS = new Set(['instructions', 'system']);
8
8
  function isSystemPromptProp(name) {
9
9
  return name !== null && exports.SYSTEM_PROMPT_PROPS.has(name);
10
10
  }
11
- function getStaticPropName(key) {
12
- if (key.type === eslint_devkit_1.AST_NODE_TYPES.Identifier)
13
- return key.name;
14
- if (key.type === eslint_devkit_1.AST_NODE_TYPES.Literal && typeof key.value === 'string')
15
- 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
+ }
16
19
  return null;
17
20
  }