본문 바로가기

보안 모범 사례와 취약점 스캐닝

최소 권한·입출력 검증·보안 헤더·의존성 관리를 적용하고 SAST·DAST·패키지 스캔으로 취약점을 찾습니다.

앞 글에서는 운영 로그와 감사 추적을 다뤘습니다. 10장의 마지막인 이 글에서는 NestJS 애플리케이션 전반의 보안 수준을 높이는 보안 모범 사례와, 숨어 있는 취약점을 찾아내는 취약점 스캐닝(vulnerability scanning)을 다룹니다.

보안은 한 번 설정하고 끝나는 일이 아니라 계속 관심을 기울여야 하는 영역입니다. 완벽한 보안 시스템은 없으므로, 여러 위협에 대비하고 취약점을 먼저 찾아 고치는 습관이 중요합니다.

NestJS 보안 통제를 요청, 응답과 브라우저, 런타임 오류와 로그, 시크릿과 IAM, 공급망의 다섯 집행 경계로 나누고 각 통제가 하는 일과 하지 않는 일을 구분한 카탈로그

Nest · Security control catalog

보안 통제의 역할과 집행 경계

요청·응답·오류와 로그·워크로드 권한·공급망의 집행 경계를 나눕니다. 각 통제의 역할과 추가 조건을 비교하고 빈틈을 담당할 책임자를 정합니다.

집행 경계별 보안 통제와 추가 조건
집행 경계통제와 역할한계와 추가 조건
요청 경계

server

ValidationPipe는 서버에서 DTO 타입·제약과 허용 필드를 검사하고, Guard는 인증된 주체가 라우트 작업을 수행할 수 있는지 판단합니다.클라이언트 검증은 UX 보조입니다. Pipe는 권한·출력 문맥을, Guard는 DB·클라우드 IAM을 대신하지 않습니다.
응답 경계

browser

텍스트는 HTML·속성·URL·JavaScript 등 대상 문맥에 맞춰 인코딩합니다. 제한된 HTML을 허용한다면 인코딩 대신 검증된 살균 정책이 필요합니다.Helmet은 라우트보다 먼저 브라우저 보안 헤더를 설정합니다. CSP는 앱별 정책이고, HSTS는 신뢰된 HTTPS 배포와 도메인 범위 검토가 전제입니다.
런타임·오류·로그 경계

app

ExceptionFilter는 상태 코드와 안전한 클라이언트 오류 스키마를 통제하고, 로그 허용 목록과 마스킹은 비밀·개인정보를 관측 데이터에서 제외합니다.필터는 인증이나 복구 로직이 아니며, 마스킹도 원본 접근 통제를 대신하지 않습니다. 상세 원인은 제한된 서버 로그로 분리합니다.
시크릿·IAM 경계

platform

외부 시크릿 저장소에서 주입·회전·감사하고, 워크로드 ID에는 DB·클라우드에서 필요한 작업만 허용합니다.ConfigModule은 설정 로딩 도구이지 비밀 저장소가 아닙니다. 사용자 Guard와 서비스 IAM은 서로 다른 주체와 자원을 통제하며, 장기 키보다 짧은 수명 자격 증명을 선호합니다.
공급망 경계

CI

잠금 파일, 패키지 출처와 업데이트 차이를 검토하고, 의존성 스캔 결과를 도달성·노출·영향과 함께 위험 기반 릴리스 조건으로 평가합니다.권고 스캐너는 알려진 취약점 후보를 찾습니다. 오탐 검증과 기한 있는 예외가 필요하며 무결성·악성 동작·런타임 공격 전체를 보장하지 않습니다.

01 · server · run

요청 경계

ValidationPipe는 서버에서 DTO 타입·제약과 허용 필드를 검사하고, Guard는 인증된 주체가 라우트 작업을 수행할 수 있는지 판단합니다.

클라이언트 검증은 UX 보조입니다. Pipe는 권한·출력 문맥을, Guard는 DB·클라우드 IAM을 대신하지 않습니다.

02 · browser · response

응답 경계

텍스트는 HTML·속성·URL·JavaScript 등 대상 문맥에 맞춰 인코딩합니다. 제한된 HTML을 허용한다면 인코딩 대신 검증된 살균 정책이 필요합니다.

Helmet은 라우트보다 먼저 브라우저 보안 헤더를 설정합니다. CSP는 앱별 정책이고, HSTS는 신뢰된 HTTPS 배포와 도메인 범위 검토가 전제입니다.

03 · app · run

런타임·오류·로그 경계

ExceptionFilter는 상태 코드와 안전한 클라이언트 오류 스키마를 통제하고, 로그 허용 목록과 마스킹은 비밀·개인정보를 관측 데이터에서 제외합니다.

필터는 인증이나 복구 로직이 아니며, 마스킹도 원본 접근 통제를 대신하지 않습니다. 상세 원인은 제한된 서버 로그로 분리합니다.

04 · platform · run

시크릿·IAM 경계

외부 시크릿 저장소에서 주입·회전·감사하고, 워크로드 ID에는 DB·클라우드에서 필요한 작업만 허용합니다.

ConfigModule은 설정 로딩 도구이지 비밀 저장소가 아닙니다. 사용자 Guard와 서비스 IAM은 서로 다른 주체와 자원을 통제하며, 장기 키보다 짧은 수명 자격 증명을 선호합니다.

05 · ci · merge / release

공급망 경계

잠금 파일, 패키지 출처와 업데이트 차이를 검토하고, 의존성 스캔 결과를 도달성·노출·영향과 함께 위험 기반 릴리스 조건으로 평가합니다.

권고 스캐너는 알려진 취약점 후보를 찾습니다. 오탐 검증과 기한 있는 예외가 필요하며 무결성·악성 동작·런타임 공격 전체를 보장하지 않습니다.

집행 주체와 시점: 애플리케이션 코드는 요청·응답·오류 경계에서 매 실행마다, 플랫폼은 자격 증명 사용 시, CI와 책임자는 병합·릴리스 시 통제합니다. 예외는 소유자·근거·만료일을 기록해야 합니다.

위 그림은 NestJS의 보안 통제를 요청, 응답과 브라우저, 실행 중 오류와 로그, 비밀과 IAM, 공급망이라는 다섯 영역으로 나누어 정리한 목록입니다. 영역마다 어떤 통제가 무엇을 하고 무엇을 하지 않는지가 함께 적혀 있어, 예를 들어 DTO 검증이 출력 인코딩을 대신하지 않고 guard가 데이터베이스 권한을 대신하지 않는다는 점을 한눈에 볼 수 있습니다.


일반적인 보안 모범 사례

최소 권한 원칙

사용자, 시스템, 프로세스 같은 모든 주체에게 맡은 일을 하는 데 필요한 최소한의 권한만 줍니다.

  • 데이터베이스 사용자: 애플리케이션이 데이터베이스에 접근할 때는 SELECT, INSERT, UPDATE, DELETE처럼 필요한 권한만 주고, DROP TABLE 같은 관리자 권한은 주지 않습니다.
  • API 키: 각 키에는 그 서비스를 쓰는 데 필요한 최소한의 권한만 줍니다.
  • 서비스 계정: 클라우드의 IAM 역할이나 서비스 계정에도 필요한 자원에 대한 최소한의 권한만 설정합니다.

NestJS에서는 데이터베이스 연결에 쓰는 계정, 외부 API 연동에 쓰는 자격 증명에 이 원칙을 적용합니다.

입력값 검증

외부에서 들어오는 데이터는 믿을 수 없다고 가정하고, 서버에서 예상한 타입, 형식, 범위, 허용 필드를 검사합니다. 검증은 데이터가 정해 둔 형식에 맞는지 확인하는 일이고, 살균은 제한적으로 허용한 콘텐츠에서 위험한 부분을 제거하는 일이며, 출력 인코딩은 값이 HTML·속성·URL·JavaScript 같은 곳에서 코드로 해석되지 않게 만드는 별도의 단계입니다.

검증만으로 SQL Injection, XSS, Command Injection이 모두 해결되지는 않습니다. 데이터베이스의 매개변수 바인딩, 셸 호출 피하기, 출력 위치별 처리처럼 값이 쓰이는 곳마다의 통제가 함께 필요합니다.

NestJS에서는 5장에서 다룬 DTO와 class-validator, class-transformer로 입력을 검증합니다.

src/dto/create-user.dto.ts
import { IsEmail, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  name: string;

  @IsEmail()
  email: string;

  @IsInt()
  @Min(18)
  @Max(100)
  age: number;
}

main.ts에서 app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }))로 전역 DTO 검증을 켭니다. whitelist는 검증 데코레이터가 없는 속성을 제거하고, forbidNonWhitelisted는 그런 속성이 들어오면 요청을 거부합니다.

출력 이스케이프와 살균

신뢰할 수 없는 값을 출력할 때는 HTML 본문, HTML 속성, URL, CSS, JavaScript처럼 실제로 들어가는 위치에 맞는 인코딩을 적용합니다. 사용자가 작성한 HTML 일부를 의도적으로 허용한다면, 모두 이스케이프하는 대신 검증된 허용 목록 기반의 살균이 필요합니다. 위치에 맞지 않는 인코딩이나 지나치게 느슨한 살균 정책은 XSS를 허용할 수 있으며, CSP는 추가 방어선일 뿐 안전한 출력 처리를 대신하지 않습니다.

백엔드가 주로 JSON API를 제공한다면 이스케이프는 주로 프런트엔드 프레임워크가 맡습니다. React, Angular, Vue는 텍스트를 DOM에 넣을 때 기본적으로 이스케이프합니다. 하지만 NestJS가 HTML을 직접 렌더링한다면 템플릿과 출력 위치에 맞는 인코딩을 적용해야 하고, 다른 서비스로 값을 넘길 때도 그 서비스의 쿼리·명령·마크업 형식에 맞는 안전한 API를 써야 합니다.

보안 헤더

HTTP 응답에 보안 관련 헤더를 붙여 브라우저가 특정 보안 정책을 적용하게 합니다.

  • Content-Security-Policy(CSP): 어떤 출처의 스크립트, 스타일시트, 이미지를 불러올 수 있는지 브라우저에 알려 줍니다. 일부 XSS의 실행과 영향을 줄이는 추가 방어선입니다.
  • X-Frame-Options: 페이지가 <iframe> 같은 요소에 포함되는 것을 제어해 클릭재킹을 막습니다.
  • X-Content-Type-Options: 브라우저가 응답의 콘텐츠 유형을 추측하는 MIME sniffing을 막습니다.
  • Strict-Transport-Security(HSTS): 브라우저가 이후 접속을 HTTPS로만 하게 합니다. HTTPS 응답으로 전달되어야 효과가 있으며, includeSubDomains와 preload는 모든 하위 도메인이 HTTPS를 지원하는지 확인한 뒤 적용합니다.
  • Referrer-Policy: Referer 헤더에 어떤 정보를 담을지 제어합니다.

앞의 CORS, CSRF, XSS 대응에서 본 것처럼, NestJS 12.1부터는 별도 패키지 없이 app.useSecurityHeaders()가 Helmet 8과 같은 기본값으로 이 헤더들을 설정합니다. 기본값에는 이미 X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, X-Frame-Options: SAMEORIGIN이 들어 있으므로, 바꾸고 싶은 항목만 옵션으로 넘깁니다.

src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // route보다 먼저, create() 바로 뒤에서 호출합니다.
  app.useSecurityHeaders({
    contentSecurityPolicy: {
      directives: {
        // 인라인 스크립트가 필요하면 nonce나 hash를 따로 설계합니다.
        scriptSrc: ["'self'"],
        objectSrc: ["'none'"],
      },
    },
    xFrameOptions: { action: 'deny' }, // X-Frame-Options: DENY
    // preload 등록은 모든 하위 도메인을 확인한 뒤 따로 검토합니다.
    strictTransportSecurity: { maxAge: 31536000, includeSubDomains: true, preload: false },
  });

  await app.listen(process.env.PORT ?? 3000);
}

void bootstrap();

NestJS 12.1보다 이전 버전이라면 Express에서 helmet 패키지를 전역 middleware로 등록해 같은 헤더를 설정할 수 있습니다. 어느 방법이든 CSP와 HSTS의 범위는 애플리케이션과 배포 환경에 맞게 확인합니다.

인증과 인가 강화

인증(authentication)은 사용자의 신원을 확인하는 과정이고, 인가(authorization)는 인증된 사용자가 특정 자원이나 작업에 접근할 수 있는지 결정하는 과정입니다. 인가에는 역할 기반 접근 제어(RBAC)나 속성 기반 접근 제어(ABAC)를 씁니다.

  • Passport: NestJS는 @nestjs/passport로 JWT, OAuth, Local 같은 인증 방식을 통합합니다.
  • guard: route handler가 실행되기 전에, 인증된 주체가 그 작업을 할 수 있는지 판단합니다. guard는 애플리케이션 사용자의 접근 정책을 집행할 뿐, 데이터베이스나 클라우드 워크로드의 IAM을 대신하지 않습니다.
  • interceptor: 요청·응답 변환이나 관측 같은 공통 관심사를 맡을 수 있지만, 인증과 인가 결정을 guard 대신 interceptor 안에 숨기지 않습니다.

비밀 관리

데이터베이스 비밀번호, API 키, 인증서 같은 민감한 정보를 안전하게 저장하고 접근을 통제합니다. 코드에 직접 적거나, Git에 커밋하거나, 안전하지 않은 곳에 환경 변수로 저장하면 심각한 위험이 생깁니다. 앞 글에서 다룬 것처럼 ConfigModule은 설정을 불러오고 검증하는 도구일 뿐 비밀 저장소가 아니므로, 운영에서는 AWS Secrets Manager, Google Cloud Secret Manager, HashiCorp Vault 같은 전용 저장소나 플랫폼의 워크로드 자격 증명을 쓰고, 짧은 수명, 최소 권한, 교체, 접근 감사를 함께 설계합니다.

의존성 보안

프로젝트가 쓰는 외부 라이브러리와 프레임워크의 알려진 취약점을 계속 확인하고 업데이트합니다. 오픈소스 라이브러리의 취약점은 애플리케이션에 그대로 영향을 줍니다.

  • npm audit: lockfile이 나타내는 의존성 트리를 기준으로 레지스트리의 알려진 취약점 권고를 조회합니다. 기본적으로 lockfile이 필요하며, 자동으로 고칠 수 없거나 사람이 검토해야 하는 결과도 있습니다.
  • Dependabot: GitHub 저장소의 의존성 업데이트를 감지하고 보안 취약점을 고치는 PR을 만들어 줍니다.
  • Snyk, Mend: 더 깊은 분석과 자동화된 작업 흐름을 제공하는 상용 의존성 스캐너입니다.

Node.js, NestJS, 그리고 모든 @nestjs/* 패키지를 포함한 의존성은 보안 패치가 적용된 버전으로 꾸준히 업데이트합니다. 7장에서 본 것처럼 NestJS의 주 버전을 올릴 때는 @nestjs/* 패키지를 한꺼번에 같은 주 버전으로 맞춥니다.

오류 처리와 정보 노출 방지

자세한 오류 메시지나 스택 트레이스는 공격자에게 시스템 내부 정보를 알려 줄 수 있습니다. 그래서 운영 환경에서는 사용자에게 일반적인 오류 메시지만 보여 주고, 자세한 정보는 로그에만 남깁니다. NestJS의 예외 필터로, 잡기로 정한 예외의 HTTP 상태와 응답 형식을 통제합니다. 필터가 인증이나 오류 복구를 대신하지는 않습니다. 조사에 필요한 원인은 허용 목록과 마스킹을 거쳐 접근이 제한된 서버 로그에 남기고, 클라이언트 응답과 서버 진단 정보의 공개 범위를 분리합니다.

안전한 세션 관리

세션 ID는 예측할 수 없어야 하고, HttpOnly 속성으로 JavaScript의 접근을 막고, Secure 속성으로 HTTPS에서만 전송되게 하며, 탈취에 대비해 유효 기간을 적절히 정합니다. Express 기반의 세션 middleware나 쿠키 설정에서 HttpOnly, Secure, SameSite, 만료 시간을 명시합니다.


취약점 스캐닝

모범 사례를 적용하는 것과 별개로, 실제 시스템에 취약점이 있는지 자동으로 점검하는 스캐닝이 필요합니다. 모범 사례는 사람이 기억하고 지킬 때만 효과가 있으므로, 리뷰에서 놓친 실수나 배포 뒤에 새로 공개된 취약점은 반복해서 돌리는 도구로 찾아야 합니다. 스캐닝은 무엇을 검사하느냐에 따라 세 가지로 나뉩니다. 소스 코드를 실행하지 않고 읽는 정적 분석, 실행 중인 애플리케이션에 요청을 보내 보는 동적 분석, 외부 패키지의 알려진 취약점을 찾는 의존성 스캐닝입니다. 셋은 서로 다른 종류의 문제를 찾으므로, 하나가 다른 것을 대신하지 않습니다.

정적 애플리케이션 보안 테스트

정적 애플리케이션 보안 테스트(SAST)는 소스 코드를 분석해 SQL Injection, XSS, 취약한 암호화 사용 같은 잠재적 취약점을 찾습니다. 코드를 실행하지 않고 분석하므로 화이트박스 테스트라고도 합니다. 개발 초기에 빠르게, 전체 코드를 검사할 수 있지만, 실행 중의 실제 경로와 설정은 볼 수 없어 잘못된 경고(false positive)와 놓친 취약점(false negative)이 모두 생길 수 있습니다.

  • 린터의 보안 규칙: NestJS 12 템플릿의 린터는 oxlint입니다. ESLint를 함께 쓰는 프로젝트라면 eslint-plugin-security 같은 플러그인으로 기본적인 보안 검사를 더할 수 있습니다.
  • SonarQube: 여러 언어를 지원하는 정적 분석 도구로, 코드 품질과 보안 취약점을 분석하고 CI/CD에 통합할 수 있습니다.
  • Semgrep: YAML 규칙으로 원하는 보안 패턴을 정의해 코드에서 찾는 패턴 기반 정적 분석 도구입니다.

동적 애플리케이션 보안 테스트

동적 애플리케이션 보안 테스트(DAST)는 실행 중인 애플리케이션에 실제로 공격을 시도해 취약점을 찾습니다. 외부에서 블랙박스처럼 테스트하므로 인증 우회나 세션 관리 문제처럼 실행 환경에서만 드러나는 취약점을 찾는 데 효과적입니다. 대신 모든 코드 경로를 시험하기 어렵고, 실제 공격 시도가 시스템과 데이터에 영향을 줄 수 있으므로, 허가받은 대상·계정·시간·속도와 중단 조건을 정한 뒤 실행합니다.

  • ZAP(예전 이름 OWASP ZAP): 널리 쓰이는 오픈소스 DAST 도구로, 자동 스캔, 퍼징, 수동 탐색을 제공합니다.
  • Burp Suite: 전문가들이 많이 쓰는 상용 도구입니다.
  • Postman, Insomnia: 잘못된 입력을 보내거나 권한 없이 접근해 보는 식으로 직접 시험할 수도 있습니다.

의존성 스캐닝

의존성 스캐닝은 프로젝트가 쓰는 npm 패키지 같은 외부 라이브러리에서 알려진 취약점을 찾습니다. npm audit은 알려진 취약점과 의존 경로, 가능한 수정 방법을 알려 주고, --audit-level로 CI가 실패할 기준을 정할 수 있습니다. 이 옵션은 보고서에서 낮은 심각도 항목을 숨기지 않고 종료 코드의 기준만 바꿉니다. Snyk와 Dependabot도 같은 목적으로 쓸 수 있으며, 실제 적용 범위와 라이선스 정책을 확인합니다.

스캐너의 결과는 확정된 판정이 아니라 조사할 후보입니다. 심각도 표시만으로 릴리스를 막거나 허용하지 말고, 실제로 그 코드에 도달하는지, 외부에 노출되는지, 악용할 수 있는지, 업무에 어떤 영향이 있는지, 다른 보완 통제가 있는지를 함께 평가합니다. 예외를 승인한다면 담당자, 이유, 보완 통제, 만료일을 기록합니다.

예를 들어 npm audit --audit-level=high를 정상적으로 실행한 결과에 Moderate 권고 한 건만 있고 High나 Critical 권고는 없다고 해 보겠습니다. 이 기준에서 종료 코드는 0일 수 있지만, 보고서에는 Moderate 항목이 그대로 남습니다. “CI 단계 성공”을 “권고 없음”이나 “서비스에 취약점 없음”으로 바꾸어 읽으면 안 되며, 담당자는 실제로 쓰는 코드 경로와 노출 여부, 수정 버전과 호환성을 확인해 조치하거나 기한이 있는 예외로 남깁니다. npm audit 공식 문서의 보고서와 종료 코드 설명을 함께 보면 --audit-level이 바꾸는 범위를 확인할 수 있습니다.

스캔 결과를 검증된 예방 규칙으로 되돌리는 보안 루프

스캔·릴리스 게이트·허가된 DAST·위험 분류/소유자·패치·재검증·예방 규칙으로 근거가 축적된다.

개발 스캔, 위험 기반 릴리스 게이트, 범위와 속도를 통제한 허가된 런타임 DAST와 관측, 탐지, 오탐과 누락 가능성을 확인하는 위험 분류와 소유자 지정, 패치, 검증, 예방 규칙이 다시 개발 단계로 이어지고 모든 단계가 공유 취약점 기록에 근거를 남기는 폐쇄 루프

Nest · Vulnerability management loop

취약점 탐지에서 재검증까지

스캔 결과의 근거와 위험을 확인하고 소유자·기한을 정해 수정합니다. 재검증과 운영 확인을 거쳐 예방 규칙을 다음 개발 단계로 되돌립니다.

개발부터 예방 규칙까지 이어지는 취약점 관리 루프 개발 중 SAST와 의존성 스캔에서 시작해 위험 기반 릴리스 게이트, 범위와 속도를 통제한 허가된 런타임 DAST와 관측, 탐지, 오탐과 누락 가능성을 확인하는 위험 분류와 소유자 지정, 패치, 재검증, 예방 규칙을 거쳐 다시 개발로 돌아오며 각 단계의 근거와 결정은 중앙 취약점 기록에 축적된다. 개발 스캔 SAST · dependency 릴리스 게이트 위험·예외 근거 허가된 DAST·관측 범위·속도 통제 탐지·정규화 중복·근거 연결 검증·분류 + 소유자 오탐/누락 · 영향·기한 패치 코드·설정·패키지 재검증 재스캔 + 회귀 테스트 예방 규칙 테스트·정책·런북 공유 취약점 기록 evidence · owner · SLA exception · verification CLOCKWISE WORK WRITE EVIDENCE TO SHARED RECORD
재검증·예방이 다음 개발로 돌아가는 폐루프 개발 스캔부터 위험 기반 릴리스 게이트, 허가된 DAST·관측, 탐지·정규화, 검증·분류와 소유자 지정, 패치, 재검증, 예방 규칙까지 이어진다. 예방 규칙은 다시 개발 스캔의 입력으로 돌아간다. 각 단계의 점선은 공유 사건에 근거와 결정을 남기는 관계이며 마지막 단계가 일괄 기록하는 구조가 아니다. 스캔 통과와 권고 부재·취약점 부재는 다른 판정이고 예외는 소유자·근거·기한이 필요하다. 개발 스캔 SAST · dependency 릴리스 게이트 위험 · 예외 근거 허가된 DAST · 관측 범위 · 속도 통제 탐지 · 정규화 중복 · 근거 연결 검증 · 분류 · 소유자 영향 · 기한 패치 코드 · 설정 · 패키지 재검증 재스캔 · 회귀 테스트 예방 규칙 테스트 · 정책 · 런북 공유 취약점 기록 evidence · owner 예외 · 검증 근거

01 · development

개발 스캔

SAST와 의존성 스캔으로 코드 패턴과 알려진 패키지 취약점 후보를 찾습니다. 통과가 취약점 부재를 뜻하지는 않습니다.

02 · release gate

위험 기반 릴리스 판단

심각도만 보지 않고 도달성, 노출, 악용 가능성, 영향과 보완 통제를 함께 평가합니다. 차단 또는 예외에는 소유자·근거·만료일이 필요합니다.

03 · runtime

허가된 DAST와 운영 관측

승인된 테스트·운영 환경에서 공격면과 실제 신호를 관측합니다. 범위와 속도를 통제해 데이터와 가용성을 보호합니다.

04 · detect

탐지·정규화

SAST, DAST, 의존성 권고와 관측 신호를 공통 사건으로 정규화하고 중복과 관련 근거를 연결합니다.

05 · triage + owner

검증·위험 분류·소유자 지정

오탐 여부를 검증하되 스캐너의 누락 가능성도 고려합니다. 노출과 영향으로 우선순위를 정하고 해결 소유자와 기한을 지정합니다.

06 · patch

코드·설정·패키지 수정

지정된 소유자가 원인에 맞는 최소 변경을 만들고 보안 검토와 일반 회귀 테스트를 함께 수행합니다.

07 · verify

재스캔 + 보안 회귀 검증

같은 탐지 경로와 재현 테스트를 다시 실행하고, 필요하면 런타임 관측까지 확인해 위험이 실제로 줄었는지 증명합니다.

08 · prevent → development

예방 규칙으로 개발에 귀환

검증된 원인을 테스트, SAST 규칙, 의존성 정책, 리뷰 기준이나 런북으로 남겨 다음 개발 스캔의 입력으로 되돌립니다.

실제 완료 조건은 “패치 PR 생성”이 아니라 수정 반영, 재검증 통과, 필요한 운영 확인, 예방 규칙 반영까지입니다. 스캐너 결과와 예외는 영구 진리가 아니라 근거와 만료일을 가진 판단으로 관리합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 개발 중 SAST와 의존성 스캔에서 시작해 위험 기반 릴리스 게이트, 범위와 속도를 통제한 허가된 런타임 DAST와 관측, 탐지, 오탐과 누락 가능성을 확인하는 위험 분류와 소유자 지정, 패치, 재검증, 예방 규칙을 거쳐 다시 개발로 돌아오며 각 단계의 근거와 결정은 중앙 취약점 기록에 축적된다.
  • 개발 스캔부터 위험 기반 릴리스 게이트, 허가된 DAST·관측, 탐지·정규화, 검증·분류와 소유자 지정, 패치, 재검증, 예방 규칙까지 이어진다. 예방 규칙은 다시 개발 스캔의 입력으로 돌아간다. 각 단계의 점선은 공유 사건에 근거와 결정을 남기는 관계이며 마지막 단계가 일괄 기록하는 구조가 아니다. 스캔 통과와 권고 부재·취약점 부재는 다른 판정이고 예외는 소유자·근거·기한이 필요하다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1스캔 결과를 검증된 예방 규칙으로 되돌리는 보안 루프
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
스캔·릴리스 gate

개발 중 수행한 SAST·의존성 스캔 결과를 위험 기반 gate로 보냅니다.

근거: 스캔 · 결정: 릴리스 gate위험이 릴리스 판단 입력이 됩니다.
허가 관측·triage

허가된 DAST·관측 뒤 오탐·누락과 소유자를 분류합니다.

DAST: 범위·속도 통제 · 기록: 소유자·위험실제 관측을 수정 책임으로 바꿉니다.
패치·재검증·예방

패치 뒤 재검증하고 예방 규칙을 개발로 돌립니다.

산출물: 검증 근거·예방 규칙 · 실행 완료: 설계와 별도같은 위험의 재발 기준을 개선합니다.

스캔 결과를 검증된 예방 규칙으로 되돌리는 보안 루프

스캔·릴리스 게이트·허가된 DAST·위험 분류/소유자·패치·재검증·예방 규칙으로 근거가 축적된다.

개발 스캔, 위험 기반 릴리스 게이트, 범위와 속도를 통제한 허가된 런타임 DAST와 관측, 탐지, 오탐과 누락 가능성을 확인하는 위험 분류와 소유자 지정, 패치, 검증, 예방 규칙이 다시 개발 단계로 이어지고 모든 단계가 공유 취약점 기록에 근거를 남기는 폐쇄 루프

Nest · Vulnerability management loop

취약점 탐지에서 재검증까지

스캔 결과의 근거와 위험을 확인하고 소유자·기한을 정해 수정합니다. 재검증과 운영 확인을 거쳐 예방 규칙을 다음 개발 단계로 되돌립니다.

개발부터 예방 규칙까지 이어지는 취약점 관리 루프 개발 중 SAST와 의존성 스캔에서 시작해 위험 기반 릴리스 게이트, 범위와 속도를 통제한 허가된 런타임 DAST와 관측, 탐지, 오탐과 누락 가능성을 확인하는 위험 분류와 소유자 지정, 패치, 재검증, 예방 규칙을 거쳐 다시 개발로 돌아오며 각 단계의 근거와 결정은 중앙 취약점 기록에 축적된다. 개발 스캔 SAST · dependency 릴리스 게이트 위험·예외 근거 허가된 DAST·관측 범위·속도 통제 탐지·정규화 중복·근거 연결 검증·분류 + 소유자 오탐/누락 · 영향·기한 패치 코드·설정·패키지 재검증 재스캔 + 회귀 테스트 예방 규칙 테스트·정책·런북 공유 취약점 기록 evidence · owner · SLA exception · verification CLOCKWISE WORK WRITE EVIDENCE TO SHARED RECORD
재검증·예방이 다음 개발로 돌아가는 폐루프 개발 스캔부터 위험 기반 릴리스 게이트, 허가된 DAST·관측, 탐지·정규화, 검증·분류와 소유자 지정, 패치, 재검증, 예방 규칙까지 이어진다. 예방 규칙은 다시 개발 스캔의 입력으로 돌아간다. 각 단계의 점선은 공유 사건에 근거와 결정을 남기는 관계이며 마지막 단계가 일괄 기록하는 구조가 아니다. 스캔 통과와 권고 부재·취약점 부재는 다른 판정이고 예외는 소유자·근거·기한이 필요하다. 개발 스캔 SAST · dependency 릴리스 게이트 위험 · 예외 근거 허가된 DAST · 관측 범위 · 속도 통제 탐지 · 정규화 중복 · 근거 연결 검증 · 분류 · 소유자 영향 · 기한 패치 코드 · 설정 · 패키지 재검증 재스캔 · 회귀 테스트 예방 규칙 테스트 · 정책 · 런북 공유 취약점 기록 evidence · owner 예외 · 검증 근거

01 · development

개발 스캔

SAST와 의존성 스캔으로 코드 패턴과 알려진 패키지 취약점 후보를 찾습니다. 통과가 취약점 부재를 뜻하지는 않습니다.

02 · release gate

위험 기반 릴리스 판단

심각도만 보지 않고 도달성, 노출, 악용 가능성, 영향과 보완 통제를 함께 평가합니다. 차단 또는 예외에는 소유자·근거·만료일이 필요합니다.

03 · runtime

허가된 DAST와 운영 관측

승인된 테스트·운영 환경에서 공격면과 실제 신호를 관측합니다. 범위와 속도를 통제해 데이터와 가용성을 보호합니다.

04 · detect

탐지·정규화

SAST, DAST, 의존성 권고와 관측 신호를 공통 사건으로 정규화하고 중복과 관련 근거를 연결합니다.

05 · triage + owner

검증·위험 분류·소유자 지정

오탐 여부를 검증하되 스캐너의 누락 가능성도 고려합니다. 노출과 영향으로 우선순위를 정하고 해결 소유자와 기한을 지정합니다.

06 · patch

코드·설정·패키지 수정

지정된 소유자가 원인에 맞는 최소 변경을 만들고 보안 검토와 일반 회귀 테스트를 함께 수행합니다.

07 · verify

재스캔 + 보안 회귀 검증

같은 탐지 경로와 재현 테스트를 다시 실행하고, 필요하면 런타임 관측까지 확인해 위험이 실제로 줄었는지 증명합니다.

08 · prevent → development

예방 규칙으로 개발에 귀환

검증된 원인을 테스트, SAST 규칙, 의존성 정책, 리뷰 기준이나 런북으로 남겨 다음 개발 스캔의 입력으로 되돌립니다.

실제 완료 조건은 “패치 PR 생성”이 아니라 수정 반영, 재검증 통과, 필요한 운영 확인, 예방 규칙 반영까지입니다. 스캐너 결과와 예외는 영구 진리가 아니라 근거와 만료일을 가진 판단으로 관리합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 개발 중 SAST와 의존성 스캔에서 시작해 위험 기반 릴리스 게이트, 범위와 속도를 통제한 허가된 런타임 DAST와 관측, 탐지, 오탐과 누락 가능성을 확인하는 위험 분류와 소유자 지정, 패치, 재검증, 예방 규칙을 거쳐 다시 개발로 돌아오며 각 단계의 근거와 결정은 중앙 취약점 기록에 축적된다.
  • 개발 스캔부터 위험 기반 릴리스 게이트, 허가된 DAST·관측, 탐지·정규화, 검증·분류와 소유자 지정, 패치, 재검증, 예방 규칙까지 이어진다. 예방 규칙은 다시 개발 스캔의 입력으로 돌아간다. 각 단계의 점선은 공유 사건에 근거와 결정을 남기는 관계이며 마지막 단계가 일괄 기록하는 구조가 아니다. 스캔 통과와 권고 부재·취약점 부재는 다른 판정이고 예외는 소유자·근거·기한이 필요하다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1스캔 결과를 검증된 예방 규칙으로 되돌리는 보안 루프
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
스캔·릴리스 gate

개발 중 수행한 SAST·의존성 스캔 결과를 위험 기반 gate로 보냅니다.

근거: 스캔 · 결정: 릴리스 gate위험이 릴리스 판단 입력이 됩니다.
허가 관측·triage

허가된 DAST·관측 뒤 오탐·누락과 소유자를 분류합니다.

DAST: 범위·속도 통제 · 기록: 소유자·위험실제 관측을 수정 책임으로 바꿉니다.
패치·재검증·예방

패치 뒤 재검증하고 예방 규칙을 개발로 돌립니다.

산출물: 검증 근거·예방 규칙 · 실행 완료: 설계와 별도같은 위험의 재발 기준을 개선합니다.

위 그림은 스캔 결과가 예방 규칙으로 돌아오기까지의 반복 과정을 보여 줍니다. 개발 단계의 스캔과 위험 기반 릴리스 검사, 허가된 범위에서의 실행 중 DAST와 관측을 거쳐 문제를 탐지하면, 잘못된 경고와 놓친 취약점의 가능성을 확인해 위험을 분류하고 담당자를 정합니다. 그 뒤 패치를 적용해 다시 검증하고, 같은 문제가 반복되지 않도록 예방 규칙을 개발 단계에 더합니다. 모든 단계는 하나의 공유된 취약점 기록을 바탕으로 진행됩니다.

보안은 애플리케이션 개발 수명 주기 전체에 녹아 있어야 하는 꾸준한 과정입니다. 최소 권한, 입력 검증, 안전한 인증과 인가 같은 기본을 지키고, SAST·DAST·의존성 스캐닝으로 취약점을 먼저 찾아 고치며, 정기적인 보안 업데이트와 새로운 위협에 대한 학습을 함께 이어 갑니다.

더 깊이: npm audit이 답하지 않는 공급망 질문

npm audit 문서에 따르면 npm audit은 프로젝트의 의존성 목록을 레지스트리에 보내 알려진 취약점에 대한 보고서를 받습니다. 그래서 아직 권고가 나오지 않은 취약점이나 막 올라온 악성 버전처럼 레지스트리가 모르는 위험은 이 보고서에 나타나지 않습니다. 같은 명령의 하위 명령인 npm audit signatures는 다른 질문에 답합니다. 내려받은 패키지가 공개 레지스트리의 서명과 맞는지 확인하고, 출처 증명(provenance attestation)이 있는 패키지라면 그 증명도 검증합니다. 문서는 증명의 형식이 바뀔 수 있으므로 최신 npm CLI로 실행하라고 안내합니다.

설치 단계 자체도 공격 경로가 될 수 있습니다. Node.js 24에 들어 있는 npm 11은 기본적으로 의존성의 package.json에 정의된 설치 수명 주기 스크립트를 실행하므로, 악성 버전이 섞이면 npm ci가 실행되는 순간 CI 환경에서 코드가 돌 수 있습니다. npm 12부터는 루트 package.json의 allowScripts가 허용하지 않은 의존성의 설치 스크립트를 기본으로 막습니다. ignore-scripts 설정을 켜면 npm이 package.json의 스크립트를 실행하지 않으므로, 설치 단계를 빌드와 테스트에서 떼어 내는 데 쓸 수 있습니다. 다만 설치할 때 컴파일이 필요한 네이티브 모듈처럼 스크립트에 기대는 패키지도 있으므로, 켜기 전에 어떤 의존성이 설치 스크립트를 쓰는지 먼저 확인합니다.


정리

  • 최소 권한 원칙에 따라 데이터베이스 계정, API 키, 서비스 계정에는 필요한 권한만 줍니다.
  • 입력 검증, 살균, 출력 인코딩은 서로 다른 단계이며, 검증만으로 SQL Injection이나 XSS가 모두 해결되지는 않습니다.
  • NestJS 12.1에서는 app.useSecurityHeaders()로 CSP, HSTS, X-Frame-Options 같은 보안 헤더를 설정합니다.
  • SAST는 코드를, DAST는 실행 중인 애플리케이션을, 의존성 스캐닝은 외부 패키지를 검사하며, 각각 잘못된 경고와 놓친 취약점이 있을 수 있습니다.
  • 스캐너 결과는 조사할 후보로 다루고, 도달 가능성·노출·영향을 평가해 조치하거나 기한이 있는 예외로 남깁니다.

연습 문제

1. 호출만 지운 취약 패키지

npm audit --audit-level=high에서 High 권고가 발견되어 audit 단계가 실패했습니다. 팀은 해당 패키지를 아직 바꾸지 않고, 소스에서 그 패키지를 호출하는 한 줄만 지웠습니다. 이 사건을 완료로 처리하려면 무엇을 확인해야 할까요?

해설 보기

호출을 지운다고 설치된 취약 패키지가 사라지지는 않습니다. lockfile과 의존 경로를 확인하고 같은 조건으로 다시 스캔해야 하며, 패키지가 여전히 남아 있다면 audit 단계는 계속 실패합니다.

호출 경로 제거를 보완 통제로 삼으려면, 다른 경로나 간접 의존으로 그 코드에 도달할 수 있는지 확인하고 회귀 테스트로 검증해야 합니다. 그래도 권고가 남은 채로 위험을 받아들이기로 했다면 담당자, 이유, 보완 통제, 만료일을 적은 예외로 다룹니다. 완료 기준은 실제로 적용한 조치와 합의한 재검증 결과에 연결합니다.

2. 스택 트레이스를 돌려주는 필터

누군가 디버깅을 쉽게 하려고 전역 예외 필터가 처리되지 않은 오류의 exception.stack을 응답 본문에 담아 돌려주도록 바꾸었고, 이 코드가 운영에 배포되었습니다. 어떤 정보가 노출될 수 있고, 어떻게 고쳐야 할까요?

해설 보기

스택 트레이스에는 서버의 파일 경로, 사용하는 라이브러리와 내부 함수 이름, 때로는 오류 메시지에 섞인 쿼리나 설정 값까지 담길 수 있습니다. 공격자는 이를 이용해 어떤 프레임워크와 버전을 쓰는지 알아내고 공격 대상을 좁힐 수 있습니다.

클라이언트에는 NestJS 기본 필터처럼 500과 일반적인 메시지만 돌려주고, 스택과 원인은 요청 ID와 함께 접근이 제한된 서버 로그에 남깁니다. 사용자는 응답에 담긴 요청 ID를 알려 주고, 운영자는 그 ID로 로그를 찾아 조사합니다.

3. migration과 애플리케이션 계정

애플리케이션이 데이터베이스 소유자 계정 하나로 연결하고, 시작할 때 migration도 같은 계정으로 실행합니다. 어떤 API에 SQL Injection 취약점이 하나 있다면, 계정을 나누었을 때와 비교해 피해가 어떻게 달라질까요?

해설 보기

소유자 계정은 테이블을 만들고 지우는 권한까지 가지므로, SQL Injection 하나로 공격자가 DROP TABLE이나 권한 변경 같은 문장까지 실행할 수 있습니다.

migration은 배포 단계에서 DDL 권한이 있는 별도 계정으로 실행하고, 애플리케이션은 필요한 테이블에 대한 SELECT, INSERT, UPDATE, DELETE만 가진 계정으로 연결하면, 같은 취약점이 있어도 피해가 데이터 조작 범위로 줄어듭니다. 물론 근본적인 해결은 매개변수 바인딩으로 취약점 자체를 없애는 것이며, 최소 권한은 피해 범위를 줄이는 추가 방어선입니다.

이것으로 10장을 마칩니다. 10장에서는 CORS·CSRF·XSS 대응, 비밀 정보 보호, 로깅과 감사 추적, 그리고 보안 모범 사례와 취약점 스캐닝까지 애플리케이션을 지키는 방법을 이어 보았습니다. 다음 장의 Docker를 이용한 컨테이너화부터는 애플리케이션을 배포하고 운영하는 방법을 다룹니다.