AgentStack
SKILL verified MIT Self-run

Trpc Security

skill-hlsitechio-claude-skills-security-trpc-security · by hlsitechio

Security audit for tRPC applications covering procedure auth via middleware, input validation with Zod, protectedProcedure vs publicProcedure patterns, router composition, context creation, batching abuse, output sanitization, and tRPC-specific patterns across Next.js, Express, Fastify, and standalone adapters. Use this skill whenever the user mentions tRPC, @trpc/server, @trpc/client, @trpc/reac…

No reviews yet
0 installs
0 views
view→install

Install

$ agentstack add skill-hlsitechio-claude-skills-security-trpc-security

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Trpc Security? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

tRPC Security Audit

Audit tRPC applications. tRPC procedures are RPC endpoints — every procedure is a public surface even if not documented.

When this skill applies

  • Reviewing tRPC router and procedure definitions
  • Auditing middleware chains (use(...)) for auth
  • Reviewing input/output schemas (Zod)
  • Checking context creation for auth resolution
  • Reviewing protected vs public procedure patterns

Workflow

Follow ../_shared/audit-workflow.md.

Phase 1: Stack detection

grep -E '"@trpc/(server|client|react-query|next)":' package.json
# Detect adapter
grep -nE 'fetchRequestHandler|createNextApiHandler|createExpressMiddleware|createHTTPServer' src/

Phase 2: Inventory

# Router definitions
grep -rn 'createTRPCRouter\|router(\|t\.router' src/ | head

# Procedures
grep -rnE 'publicProcedure|protectedProcedure|t\.procedure' src/ | head -50

# Middleware
grep -rn 't\.middleware\|\.use(' src/

# Context creation
grep -rn 'createContext\|createInnerTRPCContext' src/

# Input validation
grep -rn '\.input(' src/ | head -30

Phase 3: Detection — the checks

Context creation

The context is where auth resolution happens. Every procedure sees this.

  • TRP-CTX-1 createContext reads auth token/cookie and resolves user once per request.
  • TRP-CTX-2 Context doesn't leak secrets (DB password, internal IDs) — only resolved primitives needed by procedures.
  • TRP-CTX-3 Context creation errors don't expose stack traces to client.
export const createContext = async ({ req }: CreateContextOptions) => {
  const session = await getSessionFromRequest(req);
  return {
    db,
    user: session?.user ?? null,
    // NOT: req (raw request object — leaks too much), env secrets, etc.
  };
};
Protected vs public procedures
  • TRP-PROC-1 publicProcedure reserved for genuinely public endpoints (sign-up, login, public catalog). Everything else uses protectedProcedure.
  • TRP-PROC-2 protectedProcedure defined via middleware that throws if ctx.user is null:

``ts const isAuthed = t.middleware(({ ctx, next }) => { if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' }); return next({ ctx: { ...ctx, user: ctx.user } }); }); export const protectedProcedure = t.procedure.use(isAuthed); ``

  • TRP-PROC-3 No publicProcedure doing user-specific reads or mutations. Audit each public procedure: would it make sense for an anonymous attacker to call?
Input validation
  • TRP-IN-1 Every procedure has .input(z.object({ ... })). Procedures without .input accept anything.
  • TRP-IN-2 Input schemas use strict constraints (min/max length, format, enum). No z.any() or z.unknown().
  • TRP-IN-3 z.object({...}).strict() (rejects extras) or default strip — never .passthrough().
  • TRP-IN-4 No userId/tenantId in inputs — derive from ctx.user.
// BAD
export const updateProfile = protectedProcedure
  .input(z.object({ userId: z.string(), name: z.string() }))
  .mutation(({ input, ctx }) => {
    return ctx.db.user.update({ where: { id: input.userId }, data: { name: input.name } });
    // Attacker: { userId: 'someone-else', name: 'pwned' }
  });

// GOOD
export const updateProfile = protectedProcedure
  .input(z.object({ name: z.string().min(1).max(50) }))
  .mutation(({ input, ctx }) => {
    return ctx.db.user.update({ where: { id: ctx.user.id }, data: { name: input.name } });
  });
Authorization (per-resource)
  • TRP-AZ-1 Procedures touching specific resources verify ownership/role on that resource:

``ts export const deletePost = protectedProcedure .input(z.object({ postId: z.string().uuid() })) .mutation(async ({ input, ctx }) => { const post = await ctx.db.post.findUnique({ where: { id: input.postId } }); if (!post) throw new TRPCError({ code: 'NOT_FOUND' }); if (post.authorId !== ctx.user.id) throw new TRPCError({ code: 'FORBIDDEN' }); return ctx.db.post.delete({ where: { id: input.postId } }); }); ``

  • TRP-AZ-2 Role checks via middleware: adminProcedure = protectedProcedure.use(requireAdmin).
  • TRP-AZ-3 No procedure trusts an input field to determine "whose data" to read/write.
Output filtering
  • TRP-OUT-1 Procedures returning DB entities project to safe DTOs — no passwordHash, mfaSecret, internal flags.
  • TRP-OUT-2 Optional .output(schema) validates response shape; useful to catch accidental field leakage in code review.
Error handling
  • TRP-ERR-1 TRPCError codes used appropriately (UNAUTHORIZED 401, FORBIDDEN 403, NOTFOUND 404, BADREQUEST 400).
  • TRP-ERR-2 Error messages don't leak internal details. Custom errorFormatter strips stack traces in production.
  • TRP-ERR-3 Avoid revealing existence: return NOT_FOUND when user is not authorized to see a resource exists (vs. FORBIDDEN, which confirms existence).
const t = initTRPC.context().create({
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        // strip stack in production
        stack: process.env.NODE_ENV === 'production' ? undefined : shape.data.stack,
      },
    };
  },
});
Batching

tRPC batches multiple procedure calls into one HTTP request by default.

  • TRP-BAT-1 Batch size limits configured (maxBatchSize on the link). Default has no hard limit at server; an attacker can send 1000s of calls per request.
  • TRP-BAT-2 Rate limiting per user accounts for batched calls (count procedures, not HTTP requests).
Rate limiting
  • TRP-RL-1 Procedures (especially mutations) have rate limiting. Middleware-based limiter using Redis/Upstash:

``ts const rateLimited = t.middleware(async ({ ctx, next, path }) => { const key = rl:${ctx.user?.id ?? ctx.ip}:${path}; const { success } = await ratelimit.limit(key); if (!success) throw new TRPCError({ code: 'TOO_MANY_REQUESTS' }); return next(); }); ``

CSRF / origin handling

When tRPC procedures are called via HTTP from a browser:

  • TRP-CSRF-1 If using cookie auth, origin check applied on the handler (most adapters allow custom request inspection).
  • TRP-CSRF-2 Bearer/JWT auth doesn't need CSRF.
Subscriptions

If using tRPC subscriptions (WebSocket):

  • TRP-SUB-1 WebSocket upgrade authenticated. Subscription handlers re-check auth on emit.
  • TRP-SUB-2 Subscription topics scoped to the user; no shared global topics carrying per-user data.
Logging
  • TRP-LOG-1 Procedure input/output logging excludes sensitive fields (passwords, tokens, PII).
  • TRP-LOG-2 Slow-procedure logging captures path + duration, not full payloads.
Dependencies
  • TRP-DEP-1 tRPC v10 or v11. v9 is legacy.
  • TRP-DEP-2 Companion packages (@trpc/server, @trpc/client, @trpc/react-query) on same major version.

Phase 4: Triage

Critical: publicProcedure doing sensitive operations; procedure accepting userId from input; missing input schema; batch DoS surface.

Phase 5: Report

Use ../_shared/findings-schema.md. Prefix IDs with TRP-.

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.