프레임워크가 기본 방어를 제공해
- Origin 헤더 검증과 빌드마다 암호화되는 액션 ID로 CSRF를 방어해.
- 쓰이지 않는 서버 액션은 빌드 결과에서 제거해.
- 액션은 POST로만 호출되어 GET 주소로 노출되지 않아.
- 인라인 액션이 캡처한 값은 클라이언트에 참조가 전달되기 전에 암호화돼.
인증과 권한은 여전히 앱의 책임이야
| 위험 | 필요한 방어 |
|---|---|
| 신뢰할 수 없는 입력 | 모든 필드를 서버에서 Zod 같은 schema로 검증해. |
| 다른 사용자의 데이터 변경 | 세션을 읽고 쓰기 직전에 자원 소유권을 확인해. |
| 반복 호출 남용 | IP나 사용자 단위 rate limit을 둬. |
| closure 안의 민감한 값 | 비밀을 직접 캡처하지 말고 식별자만 받은 뒤 서버에서 다시 읽어. |
암호화는 권한 부여가 아니야
액션 ID나 묶인 값이 암호화됐다는 사실은 현재 사용자가 그 작업을 해도 된다는 증거가 아니야. 모든 액션을 인터넷에서 직접 도달 가능한 POST 엔드포인트라고 생각하고 검증·인증·권한·rate limit을 적용해.
각 액션을 공개 POST처럼 threat model 해. 로그인하지 않은 요청, 다른 사용자의 자원 ID, 너무 큰 입력, 빠른 반복 요청, 오래된 액션 참조를 실제로 보내 보고 가장 깊은 경계가 거부하는지 확인해. 감사 로그에는 사용자·자원·결과를 남기되 비밀과 원문 개인정보는 빼.
프레임워크가 제공하는 CSRF와 암호화는 앱 정책을 알지 못해. 올바른 Origin에서 온 로그인 사용자도 다른 조직의 데이터를 건드릴 수 있고, 정상 UI도 자동화로 남용될 수 있어. 기본 방어 위에 도메인 권한과 사용량 제한을 얹어야 닫힌 경계가 돼. 같은 액션을 정상 UI, 직접 만든 POST, 권한이 다른 세션에서 각각 호출해. 화면에서 버튼을 숨긴 것과 서버가 변경을 거부하는 것은 서로 다른 보안 증거야. 정상 UI를 거치지 않고 액션 POST를 재현해 Origin, 인증, 다른 사용자 ID, rate limit, 오래된 액션 참조를 차례로 시험해. 로그에는 거부 이유와 요청 ID가 남되 비밀 closure와 폼 원문이 노출되지 않는지 확인하고, 같은 요청 재전송의 결과도 봐. 보안 시험은 한 번의 수동 확인으로 끝내지 말고 위험한 액션마다 회귀 테스트로 남겨. Next.js나 인증 라이브러리를 올린 뒤에도 같은 우회 요청이 계속 거부되어야 기본 방어와 앱 정책의 결합을 믿을 수 있어.