agentsclimarketplace

Vercel ai sdk security scan

Skill Dolphinllc/claude-security-skills/skills/defensive/genai/vercel-ai-sdk-security-scan

Defensive security skills for Claude Code and the Claude Agent SDK — web applications and generative AI systems.

Install
npx -y skills add Dolphinllc/claude-security-skills --skill vercel-ai-sdk-security-scan

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Defensive security scan for the Vercel AI SDK (`ai` package). Detects unbounded experimental_attachments, streamText output rendered as HTML, tool execute handlers running shell from args, useChat endpoints without auth, generateObject schemas missing strict mode, and onFinish leaking content to telemetry. Invoke when the user asks to "review", "audit", or "scan" code using the AI SDK.

SKILL.md

5.2 KB, as published. Nobody here has run it

Vercel AI SDK Security Scan

Defensive scan for code using the Vercel AI SDK (ai, @ai-sdk/*). Reports findings using the shared scoring schema.

Scope

  • Files importing ai, @ai-sdk/openai, @ai-sdk/anthropic, @ai-sdk/google, etc.
  • Calls to streamText, generateText, streamObject, generateObject, tool, convertToCoreMessages
  • Route handlers backing useChat / useCompletion / useObject

Rules

IDSeverityDetectionFix
VAI-KEY-001criticalProvider client (createOpenAI, createAnthropic, etc.) instantiated with API key in a file marked "use client" or under app/ Client Component pathInitialize providers in route handlers / server actions only
VAI-AUTH-001highRoute handler backing useChat (POST /api/chat) has no auth check before calling streamTextVerify session/JWT at top of handler
VAI-TOOL-001criticaltool({ ... execute: async (args) => { /* shell/sql/fs from args without validation */ } })Define parameters: z.object(...) and re-validate inside execute; allowlist values
VAI-TOOL-002highTool registered but parameters is z.any() / z.object({}).passthrough()Define explicit zod schema with .strict()
VAI-OBJ-001mediumgenerateObject / streamObject called with a loose schema (z.record, z.any)Use specific zod schema; rely on the SDK's enforcement
VAI-OUT-001highstreamText output piped to dangerouslySetInnerHTML (or markdown renderer with HTML enabled) on the clientRender as text or use a sanitizing markdown renderer (rehype-sanitize)
VAI-OUT-002criticalServer-side use of model output as code (new Function, eval, shell exec)Never; parse into schema first
VAI-ATT-001highexperimental_attachments accepted from client without size / MIME / count limitsValidate on the server before forwarding to model
VAI-ATT-002mediumAttachments forwarded to model with URLs originating from arbitrary user inputAllowlist hosts; or download/validate server-side
VAI-INJ-001highsystem prompt built by string-concatenating request data (e.g., user-provided role/persona)Use parameterized templates with allowlisted values
VAI-INJ-002mediumRAG context passed inline without instruction-vs-data delimitersWrap retrieved chunks in tagged blocks; instruct model to treat as data
VAI-LOG-001mediumonFinish({ text, usage, ... }) callback ships text to remote telemetry / 3rd-party loggerLog usage/finishReason only; redact text
VAI-RATE-001medium/api/chat exposed without rate limitingApply per-user/IP limiter (Upstash / Vercel KV)
VAI-EDGE-001mediumRoute uses runtime: 'edge' and reads Node-only secrets via process.env (works today but risk of build-time leakage if moved to client)Mark runtime = 'nodejs' explicitly when using server-only secrets

Wrong vs. right

VAI-TOOL-001 (unvalidated tool execute)

// ❌ Args go straight to shell
tools: {
  run: tool({
    description: 'Run a command',
    parameters: z.object({ cmd: z.string() }),
    execute: async ({ cmd }) => {
      const { stdout } = await execAsync(cmd);
      return stdout;
    },
  }),
}
// ✅ Allowlisted enum + dispatcher
tools: {
  run: tool({
    description: 'Run an allowlisted task',
    parameters: z.object({ task: z.enum(['build', 'test', 'lint']) }).strict(),
    execute: async ({ task }) => runAllowlisted(task),
  }),
}

VAI-OUT-001 (HTML rendering)

// ❌ Prompt-injection → XSS
<div dangerouslySetInnerHTML={{ __html: message.content }} />
// ✅ Plain text or sanitized markdown
<ReactMarkdown
  remarkPlugins={[remarkGfm]}
  rehypePlugins={[rehypeSanitize]}>
  {message.content}
</ReactMarkdown>

VAI-AUTH-001 (no auth on chat endpoint)

// ❌ Anyone with the URL gets to spend your tokens + access tools
export async function POST(req: Request) {
  const { messages } = await req.json();
  return streamText({ model, messages, tools }).toDataStreamResponse();
}
// ✅ Auth + per-tenant rate limit
export async function POST(req: Request) {
  const session = await auth();
  if (!session?.user) return new Response('Unauthorized', { status: 401 });
  const { success } = await ratelimit.limit(session.user.id);
  if (!success) return new Response('Too many requests', { status: 429 });
  const { messages } = await req.json();
  return streamText({ model, messages, tools, system: SYSTEM_PROMPT })
    .toDataStreamResponse();
}

References

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.