본문 바로가기
Q&A2026년 5월 29일2분 읽기

tRPC 12 + Server Actions — createCaller가 RSC에서 권한 체크 통과돼서 보안 사고날 뻔

최
최예린
조회 505댓글 2

tRPC 12의 createCallerFactory로 RSC에서 procedure를 직접 호출하는 패턴을 도입했습니다. 그런데 일부 procedure가 권한 체크 미들웨어를 건너뛰는 걸 발견했습니다. 보안팀이 발견하기 전에 패치하긴 했는데 어떻게 정착시켜야 할지 모르겠습니다.

// server/trpc.ts
export const createCaller = createCallerFactory(appRouter)

// app/dashboard/page.tsx
import { createCaller } from '@/server/trpc'
export default async function Page() {
  const caller = createCaller({})  // ← ctx 빈 객체!
  const data = await caller.user.profile()  // 권한 체크 통과
  return <Profile data={data} />
}

실수로 createCaller({}) 빈 컨텍스트로 호출하니 procedure 내부의 ctx.user가 undefined인데도 권한 체크 통과. 모든 사용자 데이터에 접근 가능한 상태였습니다.

이런 실수를 빌드 단계에서 막을 방법이 있을까요?

댓글 2

김김영삼5개월 전

아주 흔한 함정이고 보안 사고로 이어집니다. 사용하시던 패턴을 두 단계로 안전하게 잠가야 합니다.


1단계 — ctx를 반드시 검증하는 미들웨어


procedure 자체에서 ctx.user를 항상 검증하는 baseProcedure를 만들고, 모든 procedure는 거기서 파생.


// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server'

const t = initTRPC.context<Context>().create()

const baseProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session?.user?.id) {
throw new TRPCError({ code: 'UNAUTHORIZED' })
}
return next({ ctx: { ...ctx, user: ctx.session.user } })
})

export const protectedProcedure = baseProcedure
export const publicProcedure = t.procedure

2단계 — createCaller 헬퍼로 ctx 강제


raw createCallerFactory를 직접 export하지 말고, ctx를 자동 구성하는 헬퍼로 감싸세요.


// server/caller.ts
import { headers, cookies } from 'next/headers'
import { createCallerFactory } from '@trpc/server'
import { appRouter } from './router'
import { getServerSession } from './auth'

const _caller = createCallerFactory(appRouter)

export async function getCaller() {
const session = await getServerSession({
headers: await headers(),
cookies: await cookies(),
})
return _caller({ session })
}

RSC에서는 항상 getCaller()를 사용. createCallerFactory를 직접 import 하면 ESLint 룰로 차단.


3단계 — ESLint 룰


// eslint.config.js
rules: {
'no-restricted-imports': ['error', {
paths: [{
name: '@trpc/server',
importNames: ['createCallerFactory'],
message: '직접 사용 금지. lib/caller의 getCaller() 사용.'
}]
}]
}

이렇게 하면 실수로 빈 ctx로 caller 만드는 게 빌드 단계에서 차단됩니다. 저희도 같은 사고 한 번 겪고 이 패턴으로 굳혔습니다.

보보안전문가태호5개월 전

보안 관점에서 한 가지 추가 — protectedProcedure의 미들웨어 안에서 IP rate limit도 같이 걸어두면 사고 방지에 한 겹 더 안전합니다. 누가 실수로 ctx 빈 상태로 caller 만들어도 분당 N회 이상 호출 시 자동 차단.

Ctrl+Enter로 등록