본문 바로가기

CORS, CSRF, XSS 대응

CORS·CSRF·XSS가 발생하는 신뢰 경계를 구분하고 출처 제한, 요청 의도 검증, 출력 컨텍스트 방어를 적용합니다.

9장에서는 NestJS 애플리케이션의 성능과 운영 상태를 다뤘습니다.

10장에서는 애플리케이션의 신뢰 경계를 지키는 보안(Security)을 살펴봅니다.

첫 주제는 이름이 자주 함께 등장하지만 서로 다른 문제를 해결하는 CORS, CSRF, XSS입니다.

CORS, CSRF, XSS가 지키는 신뢰 경계와 Nest 적용 지점 비교

Nest · Trust boundaries

CORS·CSRF·XSS의 세 신뢰 경계

허용되지 않은 응답 읽기, 사용자 의도 없는 쓰기, 데이터의 코드 해석을 각 경계에서 막습니다. 출처 제한·요청 의도 검증·출력 처리를 따로 적용하고 실패 증거를 확인합니다.

위협별 실패 신호, Nest·렌더러 적용 지점, 성공 증거와 방어 범위
위협 방어 · 적용 지점 성공을 확인할 증거 대신하지 않는 것
CORS app.enableCors()에서 exact allowlist 또는 엄격한 predicate, 메서드, allowedHeaders, credentials, exposedHeaders를 구성합니다. Express는 cors, Fastify는 @fastify/cors를 사용합니다. 거부한 Origin은 응답을 읽지 못하고, 허용한 credentialed Origin에는 와일드카드가 아닌 해당 출처가 반환됩니다. 요청 헤더 허용과 응답 헤더 노출도 따로 검사합니다. 인증, 인가, 서버 간 접근 제어, CSRF 방어가 아닙니다.
CSRF unsafe method에서 세션에 결합된 토큰을 검증하고 Origin·Fetch Metadata·SameSite를 보조 신호로 사용합니다. Express와 Fastify는 쿠키·세션 플러그인과 보호 미들웨어의 조합·순서가 다릅니다. 유효한 세션이어도 토큰이 없거나 틀린 POST·PUT·PATCH·DELETE는 실패합니다. safe method는 상태를 바꾸지 않습니다. CORS, XSS 방어, 인증·인가를 대신하지 않습니다.
XSS HTML 텍스트·속성·URL·JavaScript마다 맞는 인코딩과 safe sink를 사용합니다. 허용하는 raw HTML은 sanitizer를 거치며 React의 dangerouslySetInnerHTML과 SSR 우회 경계를 검토합니다. 공격 payload가 코드가 아니라 데이터로 렌더링됩니다. raw HTML 허용 경로는 sanitizer 정책을 통과하고 Helmet·CSP는 추가 방어선으로 동작합니다. DTO 입력 검증, HttpOnly, CSP만으로 출력 컨텍스트 방어를 대신할 수 없습니다.
CORS · 교차 출처 응답 노출
방어 · 적용 지점
app.enableCors()에서 exact allowlist 또는 엄격한 predicate, 메서드, allowedHeaders, credentials, exposedHeaders를 구성합니다. Express는 cors, Fastify는 @fastify/cors를 사용합니다.
성공을 확인할 증거
거부한 Origin은 응답을 읽지 못하고, 허용한 credentialed Origin에는 와일드카드가 아닌 해당 출처가 반환됩니다. 요청 헤더 허용과 응답 헤더 노출도 따로 검사합니다.
대신하지 않는 것
인증, 인가, 서버 간 접근 제어, CSRF 방어가 아닙니다.
CSRF · ambient cookie가 붙은 상태 변경
방어 · 적용 지점
unsafe method에서 세션에 결합된 토큰을 검증하고 Origin·Fetch Metadata·SameSite를 보조 신호로 사용합니다. Express와 Fastify는 쿠키·세션 플러그인과 보호 미들웨어의 조합·순서가 다릅니다.
성공을 확인할 증거
유효한 세션이어도 토큰이 없거나 틀린 POST·PUT·PATCH·DELETE는 실패합니다. safe method는 상태를 바꾸지 않습니다.
대신하지 않는 것
CORS, XSS 방어, 인증·인가를 대신하지 않습니다.
XSS · 데이터가 코드로 해석되는 출력
방어 · 적용 지점
HTML 텍스트·속성·URL·JavaScript마다 맞는 인코딩과 safe sink를 사용합니다. 허용하는 raw HTML은 sanitizer를 거치며 React의 dangerouslySetInnerHTML과 SSR 우회 경계를 검토합니다.
성공을 확인할 증거
공격 payload가 코드가 아니라 데이터로 렌더링됩니다. raw HTML 허용 경로는 sanitizer 정책을 통과하고 Helmet·CSP는 추가 방어선으로 동작합니다.
대신하지 않는 것
DTO 입력 검증, HttpOnly, CSP만으로 출력 컨텍스트 방어를 대신할 수 없습니다.

실패 지점을 분리하면 운영 기준도 선명해집니다. CORS는 응답 노출, CSRF는 쓰기 의도, XSS는 렌더링 컨텍스트에서 검증합니다.


세 방어를 먼저 분리하기

  • CORS는 브라우저가 다른 출처의 응답을 JavaScript에 노출할 수 있는지 제어합니다.
  • CSRF 방어는 쿠키 같은 ambient credential이 붙은 상태 변경 요청에 사용자의 의도가 있었는지 검증합니다.
  • XSS 방어는 신뢰할 수 없는 데이터가 브라우저의 HTML·속성·URL·JavaScript 같은 실행 컨텍스트로 해석되지 않게 합니다.

CORS는 인증이나 인가가 아니며 CSRF 토큰을 대신하지 않습니다. 입력 검증도 출력 인코딩이나 HTML 살균을 대신하지 않습니다.

하나의 미들웨어로 세 문제를 한꺼번에 해결하려 하지 말고, 각 경계에서 별도의 실패 신호를 확인해야 합니다.


CORS: 브라우저의 교차 출처 응답 노출 정책

출처(origin)는 스킴, 호스트, 포트의 조합입니다.

동일 출처 정책은 다른 출처의 응답을 브라우저 JavaScript가 마음대로 읽지 못하게 합니다. CORS 응답 헤더는 이 제한을 선택적으로 완화합니다.

이때 중요한 점은 CORS가 서버로 향하는 모든 교차 사이트 요청을 막는 방화벽이 아니라는 것입니다. 브라우저는 simple request를 전송할 수 있고 응답 읽기만 차단할 수 있으므로, 서버는 모든 요청에 인증·인가를 수행해야 합니다.

Nest에서 출처를 명시적으로 허용하기

Nest는 Express 어댑터에서 cors, Fastify 어댑터에서 @fastify/cors를 사용합니다. 두 경우 모두 app.enableCors()로 설정하지만, 실제 지원 옵션과 기본값은 선택한 어댑터 버전의 문서를 함께 확인해야 합니다.

운영 환경에서는 문자열 전체가 일치하는 출처 목록이나 같은 수준으로 엄격한 predicate를 사용합니다. 요청의 Origin 값을 검증 없이 그대로 반사해서는 안 됩니다.

src/main.ts
const allowedOrigins = new Set([
  'https://app.example.com',
  'https://admin.example.com',
]);

app.enableCors({
  origin(origin, callback) {
    // Origin이 없으면 CORS가 적용되는 교차 출처 브라우저 요청이 아니다.
    // 이 분기는 인증·인가를 생략한다는 뜻이 아니다.
    if (origin === undefined || allowedOrigins.has(origin)) {
      callback(null, true);
      return;
    }

    callback(new Error('Origin is not allowed by CORS'), false);
  },
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  credentials: true,
  allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'],
  exposedHeaders: ['X-Request-Id'],
});
  • allowedHeaders는 브라우저가 실제 요청에 보낼 수 있도록 preflight에서 허용할 요청 헤더입니다.
  • exposedHeaders는 응답을 받은 JavaScript가 읽을 수 있도록 추가로 공개할 응답 헤더입니다.
  • credentialed CORS 응답에는 Access-Control-Allow-Origin: *를 사용할 수 없습니다. 허용한 구체적 출처를 응답해야 합니다.
  • 요청의 출처에 따라 응답이 달라지면 캐시가 출처별 응답을 섞지 않도록 Vary: Origin도 확인합니다.
  • Nest 11의 Fastify CORS 기본 허용 메서드는 safelisted method로 좁아졌습니다. PUT, PATCH, DELETE가 필요하면 위처럼 명시합니다.

CORS 허용은 “이 브라우저 JavaScript가 응답을 읽어도 된다”는 뜻일 뿐, 요청자가 누구인지 또는 해당 작업을 할 권한이 있는지를 증명하지 않습니다.


CSRF: ambient credential로 보낸 쓰기 요청의 의도 확인

브라우저는 쿠키의 Domain, Path, Secure, SameSite 조건이 맞으면 요청에 쿠키를 자동으로 실을 수 있습니다. 공격자는 이 성질을 이용해 사용자가 의도하지 않은 상태 변경 요청을 유도합니다.

교차 출처 fetch가 쿠키를 요청에 포함할지는 클라이언트의 credentials mode와 쿠키 속성이 결정합니다. 서버의 구체적인 Access-Control-Allow-Origin과 Access-Control-Allow-Credentials 응답은 그 credentialed 응답을 JavaScript에 공유하기 위한 조건이지, 요청 자체의 전송 조건이 아닙니다. 폼 같은 simple request도 CORS preflight 없이 전송될 수 있으므로 CORS 설정만으로 CSRF를 막을 수 없습니다.

토큰과 요청 메타데이터를 함께 검증하기

서버는 세션에 결합되고 공격자가 예측할 수 없는 CSRF 토큰을 발급합니다.

클라이언트는 POST, PUT, PATCH, DELETE 같은 상태 변경 요청의 폼 필드나 사용자 정의 헤더에 토큰을 담습니다.

서버는 토큰을 검증하고, 필요하면 Origin 또는 Fetch Metadata를 추가 신호로 확인한 뒤에만 작업을 수행합니다.

브라우저의 상태 변경 요청
await fetch('https://api.example.com/api/items', {
  method: 'POST',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken,
  },
  body: JSON.stringify({ name: 'new item' }),
});

토큰을 사용자 정의 헤더로 보낸다면 CORS의 allowedHeaders에도 그 헤더가 있어야 합니다. 반대로 exposedHeaders에 넣는 것은 요청 전송을 허용하지 않습니다.

예시

위 fetch가 허용 출처인 https://app.example.com에서 실행되고 로그인 쿠키도 유효하다고 가정하겠습니다. CSRF 토큰이 빠졌다면, 해당 쓰기 라우트에 연결한 보호 미들웨어는 작업을 수행하기 전에 요청을 거부해야 합니다. 출처 허용과 로그인 성공은 토큰 검증 성공으로 이어지지 않습니다. 응답 상태뿐 아니라 항목이 생성되지 않았는지도 확인해야 방어가 실제로 동작했는지 알 수 있습니다.

Nest의 CSRF 통합은 HTTP 어댑터에 따라 다릅니다.

  • Express에서는 쿠키나 세션 파서를 먼저 등록한 뒤 csrf-csrf 같은 보호 미들웨어를 연결합니다.
  • Fastify에서는 쿠키 또는 세션 저장소 플러그인 뒤에 @fastify/csrf-protection을 등록합니다.

패키지별 생성·검증 API와 필수 secret/session identifier는 버전에 따라 달라질 수 있으므로 설치한 버전의 공식 API를 기준으로 구성합니다. 어느 어댑터든 보호 미들웨어의 등록 순서와 토큰 없는 쓰기 요청이 실제로 거부되는지를 테스트해야 합니다.

SameSite 쿠키, Origin 검증, Fetch Metadata는 방어를 강화하지만 애플리케이션의 사이트 구성과 호환성을 고려해야 합니다. GET, HEAD, OPTIONS는 서버 상태를 바꾸지 않도록 설계합니다.


XSS: 출력되는 컨텍스트에서 데이터를 코드와 분리하기

XSS는 신뢰할 수 없는 값이 브라우저에서 마크업이나 실행 코드로 해석될 때 발생합니다. 저장형, 반사형, DOM 기반 XSS는 유입 경로가 다르지만 방어 원칙은 같습니다.

출력 위치마다 다른 방어 사용하기

  • HTML 텍스트: 템플릿의 자동 이스케이프나 textContent처럼 데이터를 텍스트로 다루는 safe sink를 사용합니다.
  • HTML 속성: 안전한 속성만 사용하고 값을 따옴표로 감싼 뒤 속성 컨텍스트에 맞게 인코딩합니다.
  • URL: 허용할 스킴을 제한하고 각 쿼리 값은 URL 인코딩한 뒤, HTML 속성에 넣을 때는 속성 처리도 적용합니다.
  • JavaScript·CSS: 신뢰할 수 없는 값을 실행 코드에 직접 보간하지 않습니다. 피할 수 없다면 해당 컨텍스트 전용 인코더를 사용합니다.
  • 사용자 작성 HTML: raw HTML sink에 넣기 전에 검증된 HTML sanitizer와 명시적 허용 정책을 적용합니다.

React는 일반 JSX 문자열 자식과 속성 값을 이스케이프합니다. 그러나 dangerouslySetInnerHTML은 raw HTML 경계이므로 신뢰할 수 없는 HTML을 그대로 넘기면 안 됩니다. 서버 렌더링이나 다른 템플릿 엔진도 자동 이스케이프를 우회하는 기능을 별도 검토해야 합니다.

Nest의 DTO와 ValidationPipe는 요청 형태, 타입, 허용 필드를 검사하는 입력 신뢰 경계입니다. 이 검사는 HTML·속성·URL·JavaScript 출력 컨텍스트를 알지 못하므로 출력 인코딩을 대체하지 않습니다.

JSON API는 올바른 Content-Type: application/json으로 응답하고, 렌더러가 값을 어느 sink에 넣는지까지 추적합니다. HttpOnly 쿠키는 JavaScript의 쿠키 읽기를 줄이지만 XSS가 인증된 요청을 실행하는 것까지 막지는 못합니다.

예시

상품 이름으로 <b>할인</b>이라는 문자열을 받았다고 해 보겠습니다. 문자열을 허용하는 DTO를 통과했다는 사실은 이 값의 출력 방법을 정하지 않습니다. 일반 div의 textContent에 넣으면 태그를 포함한 문자열이 화면에 나타나고, 같은 div의 innerHTML에 넣으면 b 요소로 해석됩니다. 상품 이름을 텍스트로 보여 주려는 계약이라면 전자를 선택합니다. 사용자 작성 HTML을 지원하는 별도 기능은 허용할 태그·속성·URL을 정한 sanitizer가 필요합니다. script나 style 요소에는 이 일반 텍스트 사례를 그대로 적용하지 않습니다.

Helmet과 HTTPS의 경계

Helmet은 CSP를 포함한 HTTP 응답 헤더를 구성하는 도구입니다. CSP는 허용된 스크립트 출처를 제한하는 추가 방어선이지, 컨텍스트별 인코딩과 sanitizer를 대신하지 않습니다.

Express와 Fastify는 등록 방법이 다릅니다.

Express 어댑터
import helmet from 'helmet';

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
    },
  },
}));
Fastify 어댑터
import fastifyHelmet from '@fastify/helmet';

await app.register(fastifyHelmet, {
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
    },
  },
});

두 경우 모두 라우트보다 먼저 등록해야 헤더가 모든 응답에 적용됩니다.

Helmet은 응답 헤더를 설정할 뿐 TLS 연결을 만들지 않습니다. HTTPS는 애플리케이션이나 앞단 프록시에서 별도로 종료해야 하며, HSTS는 안전한 HTTPS 응답에서 보내야 브라우저가 이후 연결에 적용합니다.


요청부터 렌더링까지 확인하기

CORS의 출처 제한, CSRF의 요청 의도 검증, DTO의 입력 검증, XSS의 출력 컨텍스트 방어, Helmet의 응답 헤더는 서로 이어지지만 역할이 겹치지 않습니다.

다음 체크리스트는 기존의 경계·실패 신호와 운영 성공 기준을 한 흐름으로 합쳐, 요청이 들어와 응답이 렌더링될 때까지 누락을 확인하게 합니다.

Nest 요청·응답 보안 경계 체크리스트

preflight 여부·실제 request 전송·서버 판정·CORS 노출·렌더링으로 진행하며 서버 조건을 고정 Nest 순서로 나누지 않는다.

Nest 애플리케이션 요청부터 브라우저 렌더링까지의 보안 경계 체크리스트

Nest · Request-to-render checklist

요청부터 렌더링까지 보안 점검

CORS·인증과 인가·CSRF·입력 검증·출력 처리·응답 헤더를 역할별로 점검합니다. 번호는 점검 순서이며 실제 Nest 실행 순서는 어댑터와 미들웨어 등록 위치에 따라 확인합니다.

브라우저 전송·서버 검사·렌더링의 서로 다른 경계 부트스트랩 설정과 실제 요청을 나눈다. 필요한 preflight가 미허용이면 이 브라우저의 실제 요청은 전송되지 않는다. simple request 또는 허용된 preflight 뒤의 실제 요청은 서버의 인증·인가, 필요한 CSRF, DTO·업무 조건을 각각 통과해야 한다. 이 서버 조건들의 도식은 고정된 Nest 실행 순서가 아니다. 하나라도 실패하면 쓰기 전에 거부한다. 성공 응답도 CORS 응답 노출 조건과 응답 헤더 확인을 거치며, 렌더러는 출력 문맥에 맞춰 데이터를 다룬다. HTTPS는 별도 배포 조건이다. 원래 번호 목록은 검토 체크리스트이다. 설정 확인 미허용 허용 / simple 실패 통과 시작 시 정책 등록 CORS · Helmet · HTTPS 필요한 preflight 예정 method · header 미허용 이 실제 요청은 미전송 실제 요청 simple / 허용된 협상 서버 보호 경계 인증·인가 · CSRF DTO · 업무 조건 검사 실패 쓰기 전 서버 거부 성공 응답 JSON · 안전한 오류 브라우저 수신 응답 노출 · 헤더 확인 문맥별 렌더링 텍스트 / HTML / URL
전송 조건·서버 보호·출력 경계의 분리 부트스트랩 설정과 실제 요청을 나눈다. 필요한 preflight가 미허용이면 이 브라우저의 실제 요청은 전송되지 않는다. simple request 또는 허용된 preflight 뒤의 실제 요청은 서버의 인증·인가, 필요한 CSRF, DTO·업무 조건을 각각 통과해야 한다. 이 서버 조건들의 도식은 고정된 Nest 실행 순서가 아니다. 하나라도 실패하면 쓰기 전에 거부한다. 성공 응답도 CORS 응답 노출 조건과 응답 헤더 확인을 거치며, 렌더러는 출력 문맥에 맞춰 데이터를 다룬다. HTTPS는 별도 배포 조건이다. 원래 번호 목록은 검토 체크리스트이다. 미허용 실패 시작 시 정책 등록 CORS · Helmet · HTTPS 필요한 preflight 예정 method · header 전송 허용 여부 미허용 이 실제 요청은 미전송 실제 요청 simple / 허용된 협상 서버 보호 경계 인증·인가 · CSRF DTO · 업무 조건 검사 실패 쓰기 전에 서버 거부 성공 응답 JSON · 안전한 오류 브라우저 수신 응답 노출 · 헤더 확인 문맥별 렌더링 텍스트 / HTML / URL

요청 → 응답 → 렌더링

  1. 부트스트랩 · 출처와 헤더 정책 등록

    exact allowlist 또는 엄격한 predicate로 Origin을 판정하고 메서드, 요청용 allowedHeaders, credentials, 응답용 exposedHeaders를 분리합니다. Express·Fastify에서 CORS와 Helmet의 패키지·등록 순서를 각각 확인합니다.

  2. 선택적 preflight · 실제 요청 가능 여부 협상

    브라우저는 non-simple 교차 출처 요청 전에 Origin, 예정 메서드와 요청 헤더를 보냅니다. 서버 응답의 allowed method·header는 전송 가능 여부를 알릴 뿐 사용자 인증이나 권한을 증명하지 않습니다.

  3. 실제 요청 · 인증과 인가는 서버에서 수행

    쿠키 포함 여부는 요청의 credentials mode와 쿠키 속성이 정합니다. CORS 응답 헤더는 credentialed 응답을 JavaScript에 공유할 조건이며 요청 자체의 전송 조건이 아니므로, guard와 권한 검사로 요청 주체와 작업 권한을 확인합니다.

  4. 상태 변경 · CSRF 의도 검증

    POST·PUT·PATCH·DELETE에서 세션 결합 토큰을 검증하고 Origin·Fetch Metadata·SameSite를 보조 신호로 사용합니다. safe method는 상태를 바꾸지 않습니다.

  5. DTO 경계 · 입력 형태와 허용 필드 검증

    ValidationPipe와 DTO로 타입, 형태, 필수값, 허용 필드를 확인한 뒤 비즈니스 규칙을 적용합니다. 이 입력 검증은 이후 렌더링 컨텍스트의 출력 인코딩을 대신하지 않습니다.

  6. 응답 생성 · 데이터와 실행 컨텍스트 분리

    JSON은 올바른 content type으로 반환하고, HTML 텍스트·속성·URL·JavaScript 출력은 각 컨텍스트에 맞게 처리합니다. 일반 React JSX의 이스케이프를 유지하고 raw HTML은 sanitizer 정책을 통과시킵니다.

  7. 브라우저 수신 · 노출 헤더와 추가 방어 확인

    exposedHeaders는 JavaScript가 읽을 응답 헤더만 정합니다. Helmet·CSP·HttpOnly는 추가 방어선이며, HTTPS는 별도로 제공하고 HSTS는 안전한 응답에서 이후 연결을 위해 전달합니다.

성공 조건은 허용되지 않은 Origin, 잘못된 CSRF 토큰·DTO, 실행 가능한 출력 payload가 각각 자신의 경계에서 거부되거나 데이터로 처리되는 것입니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 부트스트랩 설정과 실제 요청을 나눈다. 필요한 preflight가 허용되지 않으면 이 브라우저의 실제 요청은 전송되지 않는다. simple request 또는 허용된 preflight 뒤의 실제 요청은 서버의 인증·인가, 필요한 CSRF, DTO·업무 조건을 각각 통과해야 한다. 이 서버 조건들의 도식은 고정된 Nest 실행 순서가 아니다. 하나라도 실패하면 쓰기 전에 거부한다. 성공 응답도 CORS 응답 노출 조건과 응답 헤더 확인을 거치며, 렌더러는 출력 문맥에 맞춰 데이터를 다룬다. HTTPS는 별도 배포 조건이다. 원래 번호 목록은 검토 체크리스트이다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1Nest 요청·응답 보안 경계 체크리스트
7부트스트랩 설정과 실제 요청을 나눈다. 필요한 preflight가 허용되지 않으면 이 브라우저의 실제 요청은 전송되지 않는다. simple request 또는 허용된 preflight 뒤의 실제 요청은 서버의 인증·인가, 필요한 CSRF, DTO·업무 조건을 각각 통과해야 한다. 이 서버 조건들의 도식은 고정된 Nest 실행 순서가 아니다. 하나라도 실패하면 쓰기 전에 거부한다. 성공 응답도 CORS 응답 노출 조건과 응답 헤더 확인을 거치며, 렌더러는 출력 문맥에 맞춰 데이터를 다룬다. HTTPS는 별도 배포 조건이다. 원래 번호 목록은 검토 체크리스트이다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
브라우저 전송 경계

필요한 preflight가 허용되지 않으면 실제 요청을 보내지 않습니다.

미허용: 전송 없음 · simple·허용: 전송브라우저 정책이 서버 도달 여부를 정합니다.
서버 조건 확인

인증·인가·필요한 CSRF·DTO·업무 조건을 검사합니다.

한 실패: 쓰기 전 거부 · 검사 총순서: 고정 안 함허용된 실제 요청만 쓰기에 도달합니다.
응답 노출·렌더

CORS 노출 조건·헤더 뒤 출력 문맥에 맞게 렌더합니다.

응답: 노출 조건 · 출력: 문맥별 처리 · HTTPS: 배포 조건서버 성공과 브라우저 사용 가능 결과를 구분합니다.

Nest 요청·응답 보안 경계 체크리스트

preflight 여부·실제 request 전송·서버 판정·CORS 노출·렌더링으로 진행하며 서버 조건을 고정 Nest 순서로 나누지 않는다.

Nest 애플리케이션 요청부터 브라우저 렌더링까지의 보안 경계 체크리스트

Nest · Request-to-render checklist

요청부터 렌더링까지 보안 점검

CORS·인증과 인가·CSRF·입력 검증·출력 처리·응답 헤더를 역할별로 점검합니다. 번호는 점검 순서이며 실제 Nest 실행 순서는 어댑터와 미들웨어 등록 위치에 따라 확인합니다.

브라우저 전송·서버 검사·렌더링의 서로 다른 경계 부트스트랩 설정과 실제 요청을 나눈다. 필요한 preflight가 미허용이면 이 브라우저의 실제 요청은 전송되지 않는다. simple request 또는 허용된 preflight 뒤의 실제 요청은 서버의 인증·인가, 필요한 CSRF, DTO·업무 조건을 각각 통과해야 한다. 이 서버 조건들의 도식은 고정된 Nest 실행 순서가 아니다. 하나라도 실패하면 쓰기 전에 거부한다. 성공 응답도 CORS 응답 노출 조건과 응답 헤더 확인을 거치며, 렌더러는 출력 문맥에 맞춰 데이터를 다룬다. HTTPS는 별도 배포 조건이다. 원래 번호 목록은 검토 체크리스트이다. 설정 확인 미허용 허용 / simple 실패 통과 시작 시 정책 등록 CORS · Helmet · HTTPS 필요한 preflight 예정 method · header 미허용 이 실제 요청은 미전송 실제 요청 simple / 허용된 협상 서버 보호 경계 인증·인가 · CSRF DTO · 업무 조건 검사 실패 쓰기 전 서버 거부 성공 응답 JSON · 안전한 오류 브라우저 수신 응답 노출 · 헤더 확인 문맥별 렌더링 텍스트 / HTML / URL
전송 조건·서버 보호·출력 경계의 분리 부트스트랩 설정과 실제 요청을 나눈다. 필요한 preflight가 미허용이면 이 브라우저의 실제 요청은 전송되지 않는다. simple request 또는 허용된 preflight 뒤의 실제 요청은 서버의 인증·인가, 필요한 CSRF, DTO·업무 조건을 각각 통과해야 한다. 이 서버 조건들의 도식은 고정된 Nest 실행 순서가 아니다. 하나라도 실패하면 쓰기 전에 거부한다. 성공 응답도 CORS 응답 노출 조건과 응답 헤더 확인을 거치며, 렌더러는 출력 문맥에 맞춰 데이터를 다룬다. HTTPS는 별도 배포 조건이다. 원래 번호 목록은 검토 체크리스트이다. 미허용 실패 시작 시 정책 등록 CORS · Helmet · HTTPS 필요한 preflight 예정 method · header 전송 허용 여부 미허용 이 실제 요청은 미전송 실제 요청 simple / 허용된 협상 서버 보호 경계 인증·인가 · CSRF DTO · 업무 조건 검사 실패 쓰기 전에 서버 거부 성공 응답 JSON · 안전한 오류 브라우저 수신 응답 노출 · 헤더 확인 문맥별 렌더링 텍스트 / HTML / URL

요청 → 응답 → 렌더링

  1. 부트스트랩 · 출처와 헤더 정책 등록

    exact allowlist 또는 엄격한 predicate로 Origin을 판정하고 메서드, 요청용 allowedHeaders, credentials, 응답용 exposedHeaders를 분리합니다. Express·Fastify에서 CORS와 Helmet의 패키지·등록 순서를 각각 확인합니다.

  2. 선택적 preflight · 실제 요청 가능 여부 협상

    브라우저는 non-simple 교차 출처 요청 전에 Origin, 예정 메서드와 요청 헤더를 보냅니다. 서버 응답의 allowed method·header는 전송 가능 여부를 알릴 뿐 사용자 인증이나 권한을 증명하지 않습니다.

  3. 실제 요청 · 인증과 인가는 서버에서 수행

    쿠키 포함 여부는 요청의 credentials mode와 쿠키 속성이 정합니다. CORS 응답 헤더는 credentialed 응답을 JavaScript에 공유할 조건이며 요청 자체의 전송 조건이 아니므로, guard와 권한 검사로 요청 주체와 작업 권한을 확인합니다.

  4. 상태 변경 · CSRF 의도 검증

    POST·PUT·PATCH·DELETE에서 세션 결합 토큰을 검증하고 Origin·Fetch Metadata·SameSite를 보조 신호로 사용합니다. safe method는 상태를 바꾸지 않습니다.

  5. DTO 경계 · 입력 형태와 허용 필드 검증

    ValidationPipe와 DTO로 타입, 형태, 필수값, 허용 필드를 확인한 뒤 비즈니스 규칙을 적용합니다. 이 입력 검증은 이후 렌더링 컨텍스트의 출력 인코딩을 대신하지 않습니다.

  6. 응답 생성 · 데이터와 실행 컨텍스트 분리

    JSON은 올바른 content type으로 반환하고, HTML 텍스트·속성·URL·JavaScript 출력은 각 컨텍스트에 맞게 처리합니다. 일반 React JSX의 이스케이프를 유지하고 raw HTML은 sanitizer 정책을 통과시킵니다.

  7. 브라우저 수신 · 노출 헤더와 추가 방어 확인

    exposedHeaders는 JavaScript가 읽을 응답 헤더만 정합니다. Helmet·CSP·HttpOnly는 추가 방어선이며, HTTPS는 별도로 제공하고 HSTS는 안전한 응답에서 이후 연결을 위해 전달합니다.

성공 조건은 허용되지 않은 Origin, 잘못된 CSRF 토큰·DTO, 실행 가능한 출력 payload가 각각 자신의 경계에서 거부되거나 데이터로 처리되는 것입니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 부트스트랩 설정과 실제 요청을 나눈다. 필요한 preflight가 허용되지 않으면 이 브라우저의 실제 요청은 전송되지 않는다. simple request 또는 허용된 preflight 뒤의 실제 요청은 서버의 인증·인가, 필요한 CSRF, DTO·업무 조건을 각각 통과해야 한다. 이 서버 조건들의 도식은 고정된 Nest 실행 순서가 아니다. 하나라도 실패하면 쓰기 전에 거부한다. 성공 응답도 CORS 응답 노출 조건과 응답 헤더 확인을 거치며, 렌더러는 출력 문맥에 맞춰 데이터를 다룬다. HTTPS는 별도 배포 조건이다. 원래 번호 목록은 검토 체크리스트이다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1Nest 요청·응답 보안 경계 체크리스트
7부트스트랩 설정과 실제 요청을 나눈다. 필요한 preflight가 허용되지 않으면 이 브라우저의 실제 요청은 전송되지 않는다. simple request 또는 허용된 preflight 뒤의 실제 요청은 서버의 인증·인가, 필요한 CSRF, DTO·업무 조건을 각각 통과해야 한다. 이 서버 조건들의 도식은 고정된 Nest 실행 순서가 아니다. 하나라도 실패하면 쓰기 전에 거부한다. 성공 응답도 CORS 응답 노출 조건과 응답 헤더 확인을 거치며, 렌더러는 출력 문맥에 맞춰 데이터를 다룬다. HTTPS는 별도 배포 조건이다. 원래 번호 목록은 검토 체크리스트이다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
브라우저 전송 경계

필요한 preflight가 허용되지 않으면 실제 요청을 보내지 않습니다.

미허용: 전송 없음 · simple·허용: 전송브라우저 정책이 서버 도달 여부를 정합니다.
서버 조건 확인

인증·인가·필요한 CSRF·DTO·업무 조건을 검사합니다.

한 실패: 쓰기 전 거부 · 검사 총순서: 고정 안 함허용된 실제 요청만 쓰기에 도달합니다.
응답 노출·렌더

CORS 노출 조건·헤더 뒤 출력 문맥에 맞게 렌더합니다.

응답: 노출 조건 · 출력: 문맥별 처리 · HTTPS: 배포 조건서버 성공과 브라우저 사용 가능 결과를 구분합니다.

보안은 설정 존재 여부가 아니라 실패 경로로 검증합니다. 허용되지 않은 출처의 응답 읽기, 토큰 없는 상태 변경, 잘못된 DTO, 악성 출력 payload, HTTPS가 아닌 HSTS 전달이 각각 기대한 지점에서 실패하는지 자동화된 테스트로 확인합니다.

스스로 확인하기

허용 출처에서 위 JSON POST를 보냅니다. 브라우저의 preflight가 필요한 조건이며, 서버는 exposedHeaders에 X-CSRF-Token을 추가했지만 allowedHeaders에서는 그 이름을 삭제했습니다. 이 변경만으로 토큰 헤더를 실제 요청에 실을 수 있을까요? preflight를 고친 뒤에도 세션에 맞지 않는 토큰을 보내면 항목을 생성해도 될까요?

헤더 방향과 서버 검증으로 나누어 풀이하기

exposedHeaders는 응답 헤더를 JavaScript가 읽는 방향입니다. 예정된 요청 헤더가 preflight 응답에서 허용되지 않으면 브라우저는 이 조건의 실제 요청을 보내지 않습니다. allowedHeaders를 고쳐 실제 요청을 보낼 수 있어도, 잘못된 CSRF 토큰은 서버의 보호 미들웨어에서 거부되어야 하고 쓰기 효과도 없어야 합니다. 두 실패 지점은 서로 다른 조건을 검사합니다.

OWASP CSRF 지침의 서버 토큰 검증과 XSS 지침의 출력 문맥·safe sink 설명을 함께 확인하면 각 테스트가 지키는 경계를 구분할 수 있습니다.