
[security] AWS WAF
정상 요청처럼 위장한 SQL 인젝션과 XSS를, WAF가 실제로 어떻게 걸러내는지 뜯어봅니다.
One-Line Summary
AWS WAF는 HTTP/HTTPS 요청(L7)을 내용까지 뜯어보고 SQL 인젝션·XSS·악성 봇·특정 국가/IP·과도한 요청 같은 웹 공격을 규칙(rule)으로 필터링하는 애플리케이션 계층 방화벽이다. CloudFront / ALB / API Gateway / AppSync / Cognito에 붙는다. 시험에서는 L7 웹 공격 차단 / 관리형 규칙(Managed Rules) / Rate-based 규칙 / 지리·IP 차단 / WAF vs Shield vs Network Firewall vs SG·NACL / 왜 L4는 못 막나 가 핵심이다.
뭔지 (What / Why)
SG·NACL은 IP·포트(L3/L4) 수준에서만 판단한다. "443 포트로 들어오는 HTTPS"는 정상 트래픽처럼 보이지만, 그 안의 요청 본문에 ' OR 1=1-- 같은 SQL 인젝션 페이로드가 들어있을 수 있다. SG·NACL은 이걸 볼 수 없다 — 포트와 IP만 맞으면 통과시킨다.
WAF는 그 HTTP 요청의 내용물(URL 경로, 헤더, 쿼리스트링, 바디, 쿠키, HTTP 메서드)을 검사해서 "이건 공격 패턴이다" 하고 막거나(Block), 통과시키거나(Allow), 세거나(Count), 캡차/챌린지를 띄운다.
클라이언트 요청 (HTTPS 443)
│ 헤더 / URI / 쿼리 / 바디 / 쿠키 …
▼
[AWS WAF] ← 요청 "내용"을 규칙과 대조 (L7)
│ Allow / Block / Count / CAPTCHA / Challenge
▼
[CloudFront / ALB / API Gateway / AppSync / Cognito]
▼
백엔드(EC2/Lambda/…)
핵심: SG·NACL은 "누가·어느 포트로" 왔는지를 보고, **WAF는 "무엇을 요청하는지(내용)"**를 본다.
왜 L7은 막는데 L4는 못 막나 (사용자 질문 — 반드시 이해)
이건 WAF가 어디에 붙어서, 무엇을 볼 수 있느냐의 문제다.
1) WAF는 L7 종단(HTTP 리버스 프록시) 위에서 동작한다
WAF는 raw 패킷을 보는 장치가 아니라, HTTP 요청이 이미 조립·복호화된 지점(CloudFront, ALB, API Gateway)에 붙는다. 이 지점에서는 이미:
- TCP 핸드셰이크가 끝나 연결이 성립되어 있고,
- TLS가 복호화되어 있으며,
- 흩어진 TCP 세그먼트가 하나의 완결된 HTTP 요청으로 재조립되어 있다.
즉 WAF가 보는 입력 단위는 "패킷"이 아니라 **"HTTP 요청 하나"**다. 그래서 URI·헤더·바디 같은 L7 필드를 검사할 수 있다.
[TCP/IP 패킷들] → (LB/CloudFront가 TLS 복호화 + HTTP 재조립) → [완성된 HTTP 요청] → [WAF가 검사]
L3/L4 영역 L7 영역 ◀── WAF는 여기부터
2) L4 공격은 "요청 내용"이 아니라 "연결·패킷 자체"라서 WAF의 검사 대상이 될 수 없다
L4(전송 계층) 공격의 대표는 SYN Flood, UDP Flood, 볼류메트릭 DDoS다. 이들의 특징:
- HTTP 요청을 완성하지 않는다. 예: SYN Flood은 TCP 핸드셰이크의 SYN만 쏟아붓고 연결을 완성하지 않는다 → 애초에 "HTTP 요청"이 만들어지지 않으니 WAF까지 도달하지 못한다.
- 페이로드에 악의가 있는 게 아니라, 물량·연결 상태 자체가 공격이다. WAF는 "이 요청 내용이 나쁜가?"를 판단하는 도구지, "초당 수백만 SYN 패킷"을 흡수하는 장치가 아니다.
- WAF가 붙는 지점(L7 프록시)에 트래픽이 도달하기 전에, 네트워크·전송 계층에서 이미 처리(흡수/차단)되어야 한다.
비유: WAF는 건물 입구에서 방문객이 들고 온 가방 속 내용물을 검사하는 보안요원이다. 그런데 L4 DDoS는 "수백만 명이 문 앞에 몰려와 문을 못 열게 하는" 상황 — 가방을 열어볼 문제가 아니라 **군중을 통제(용량/네트워크)**해야 하는 문제다.
3) 그럼 L3/L4는 누가 막나 — 계층별 분담
| 계층 | 공격 예시 | 담당 |
|---|---|---|
| L3/L4 (네트워크·전송) | SYN/UDP Flood, 볼류메트릭 DDoS, IP 스푸핑 | Shield (Standard/Advanced), Network Firewall, NACL |
| L7 (애플리케이션) | SQLi, XSS, 악성 봇, HTTP Flood, 나쁜 요청 | WAF |
- Shield Standard(무료·자동): CloudFront/Route 53/ELB 앞에서 흔한 L3/L4 DDoS를 자동 완화.
- Shield Advanced(유료): 대규모 DDoS 방어 + DDoS 대응팀(DRT) + 요금 급증 보호.
- WAF: L7 — Shield가 못 보는 "요청 내용"과 L7 HTTP Flood(rate-based) 담당.
주의: **HTTP Flood(L7 DDoS)**는 WAF의 Rate-based rule로 막는다. "DDoS니까 무조건 Shield"가 아니라, L7 요청 폭주는 WAF rate-limit, L3/L4 볼류메트릭은 Shield로 나뉜다.
핵심 구성 요소 (빈출)
| 요소 | 의미 |
|---|---|
| Web ACL | WAF의 최상위 컨테이너. 규칙들을 담아 리소스(CloudFront/ALB 등)에 연결 |
| Rule | 검사 조건 + 동작(Allow/Block/Count/CAPTCHA/Challenge) |
| Rule Group | 규칙 묶음(재사용). Managed 또는 직접 생성 |
| Managed Rules | AWS·마켓플레이스가 관리하는 사전 정의 규칙 세트(자동 업데이트) |
| Statement(조건) | IP set / geo match / string·regex match / SQLi / XSS / size / rate 등 |
| Default action | 어떤 규칙에도 안 걸린 요청의 기본 처리(Allow 또는 Block) |
Rule 동작(Action)
| Action | 의미 | 용도 |
|---|---|---|
| Allow | 통과 | 명시적 허용 |
| Block | 차단(403 반환) | 공격 차단 |
| Count | 차단 안 하고 카운트만 | 규칙 배포 전 테스트/튜닝(오탐 확인) |
| CAPTCHA | 퍼즐 요구 | 사람 여부 확인, 봇 차단 |
| Challenge | 브라우저에 조용한 챌린지(JS) | 자동화 봇 차단(사용자 개입 최소) |
실무 팁: 새 규칙은 먼저 Count로 배포해 오탐을 관찰하고, 안전 확인 후 Block으로 전환한다. (시험에도 "정상 트래픽 영향 없이 규칙 검증" → Count 로 나온다.)
규칙 조건(Statement) 종류 — 무엇을 검사하나
| 조건 | 막는 것 |
|---|---|
| IP set match | 특정 IP/CIDR 허용·차단(알려진 악성 IP) |
| Geo match | 국가 단위 허용·차단(지리 제한) |
| String / Regex match | URI·헤더·쿼리·바디의 특정 문자열/패턴 |
| SQLi match | SQL 인젝션 패턴 |
| XSS match | 크로스사이트 스크립팅 패턴 |
| Size constraint | 과도하게 큰 바디/헤더(버퍼 오버플로·남용) |
| Rate-based | IP당 5분 슬라이딩 윈도 요청 수 초과 시 차단(L7 DDoS/브루트포스) |
Rate-based rule (빈출 — L7 폭주 방어)
특정 IP가 5분(rolling) 동안 정해진 임계치를 넘으면 해당 IP를 임시 차단한다.
예: "IP 하나가 5분 내 2000 요청 초과 → Block"
→ HTTP Flood, 로그인 브루트포스, 스크래핑 방어
- 임계치 아래로 떨어지면 자동 해제. IP·헤더·쿠키 등 기준으로 세분화 가능.
- "특정 IP의 과도한 요청/브루트포스/스크래핑" = Rate-based rule.
탐지·차단은 실제로 어떻게 일어나나 (메커니즘 심화)
"WAF가 SQLi를 막는다"는 말의 실제 동작은 4단계로 일어난다. 규칙 하나는 다음을 정의한다:
① 어디를 볼까 (Inspection component) → ② 어떻게 정규화할까 (Text transformation)
→ ③ 무엇과 매칭할까 (Match statement) → ④ 걸리면 뭘 할까 (Action)
단계 ①: 요청의 "어느 부분"을 볼지 고른다 (Inspection component)
WAF는 요청 전체를 뭉뚱그려 보지 않는다. 검사 대상 컴포넌트를 명시적으로 지정한다.
| 컴포넌트 | 예 |
|---|---|
| URI path | /products/search |
| Query string / 개별 query 인자 | ?id=10의 id 값 |
| Header (단일/전체) | User-Agent, Cookie |
| Body (전체/JSON 특정 필드) | POST 본문, {"user":"..."} 의 user |
| HTTP method | GET / POST |
핵심: SQLi는 보통 query 인자·body에, 봇/스캐너는 User-Agent 헤더에, 경로 탐색은 URI에 들어온다. 그래서 규칙마다 "볼 곳"이 다르다.
바디 검사 한계 (빈출·실무 함정):
- WAF는 바디 앞부분 일부만 검사한다 — 기본 ALB/API Gateway 8 KB, CloudFront 16 KB(설정으로 상향 가능, CloudFront는 최대 64 KB).
- 그 한계를 넘는 부분은 oversize handling 옵션으로 처리:
Continue(넘는 부분 무시하고 계속) /Match(초과 자체를 매치로) /NoMatch. - 함정: 공격 페이로드를 바디 뒷부분(8KB 이후)에 숨기면 기본 설정에선 검사 밖 → 큰 바디를 다루면 한계·oversize 처리를 반드시 설계.
단계 ②: 인코딩 회피를 무력화한다 (Text Transformation) — 가장 중요
공격자는 탐지를 피하려고 페이로드를 인코딩·변형한다. 그래서 WAF는 매칭 전에 입력을 정규화(normalize)한다. 이게 없으면 단순 문자열 매칭은 쉽게 우회당한다.
공격자가 보내는 것: %27%20OR%201%3D1-- (URL 인코딩된 ' OR 1=1--)
' OR 1=1-- ← URL_DECODE 변환 후 원형이 드러남 → 매칭됨
주요 변환(규칙에 순서대로 여러 개 적용 가능):
| Transformation | 하는 일 | 막는 회피 |
|---|---|---|
| URL_DECODE | %27→' | URL 인코딩 회피 |
| HTML_ENTITY_DECODE | <→<, '→' | HTML 엔티티 회피 |
| LOWERCASE | SeLeCt→select | 대소문자 섞기 회피 |
| COMPRESS_WHITE_SPACE | 공백 다중→단일 | OR/**/1=1류 공백 조작 |
| CMD_LINE | 셸 메타문자 정규화 | 명령어 인젝션 회피 |
| BASE64_DECODE | base64 해제 | 인코딩 은닉 |
시험/실무 핵심: "정규 표현식/문자열 규칙이 인코딩된 공격을 못 잡는다" → Text Transformation을 규칙에 추가해 정규화 후 매칭. WAF가 회피에 강한 이유가 바로 이 단계다.
단계 ③: 무엇과 대조하나 — SQLi 탐지는 "키워드 블랙리스트"가 아니다
여기가 사용자가 궁금해한 **"SQL Injection을 실제로 어떻게 막나"**의 핵심이다.
순진한 방식은 "select", "union" 같은 키워드를 블랙리스트로 막는 것인데, 이건 두 가지로 실패한다:
- 오탐: 정상 검색어 "I select the best union representative"도 막아버림.
- 미탐:
SeL/**/eCT, 인코딩 등으로 쉽게 우회.
그래서 WAF의 SQLi match statement는 단어를 세는 게 아니라, 정규화된 입력을 SQL 문법 구조로 해석해 "SQL 코드로 동작 가능한가"를 판단한다. 즉 **"이 입력이 SQL 구문처럼 동작하는가"**를 본다.
정상 검색: keyword = "red shoes"
→ 토큰화하면 그냥 문자열. SQL 구문 아님 → 통과
공격: id = "10 OR 1=1--"
→ 토큰화: [숫자][논리연산자 OR][비교식 1=1][주석 --]
→ "값이 들어갈 자리에 항상 참인 조건 + 주석으로 뒷부분 무력화" = 전형적 SQLi 구조
→ Block
탐지되는 대표 구조:
- Tautology(항상 참):
OR 1=1,OR 'a'='a' - 주석으로 원 쿼리 절단:
--,#,/* */ - UNION 기반 데이터 추출:
UNION SELECT ... - 스택 쿼리·서브쿼리 삽입:
; DROP TABLE ...
정리: WAF의 SQLi 차단 = "검사 위치 지정 → 인코딩 정규화 → 입력을 SQL 문법으로 파싱해 공격 구조인지 판별 → Block". 단순 단어 매칭이 아니라 구문 해석 기반이라 회피에 강하고 오탐이 적다.
(XSS도 같은 원리 — <script>, onerror=, javascript: 같은 HTML/JS 실행 구조를 정규화 후 파싱해 판별한다.)
단계 ④: 규칙 평가 순서와 Action의 종결성 (자주 틀림)
Web ACL 안의 규칙들은 priority 번호 오름차순으로 평가되며, Action에 따라 평가가 **거기서 끝나느냐(terminating)**가 갈린다.
| Action | 종결성 | 동작 |
|---|---|---|
| Allow | 종결 | 즉시 허용, 이후 규칙 안 봄 |
| Block | 종결 | 즉시 차단(403), 이후 규칙 안 봄 |
| Count | 비종결 | 카운트만 하고 다음 규칙 계속 평가 |
| CAPTCHA / Challenge | 조건부 종결 | 토큰 유효하면 통과, 아니면 챌린지 띄우고 종결 |
priority 0: IP set (사내 IP) → Allow [종결] 사내는 여기서 통과
priority 1: Rate-based [초과 시 Block-종결]
priority 2: Managed SQLi rule → Block [SQLi면 여기서 차단]
priority 3: Bot Control
default action: 아무 규칙에도 안 걸리면 Allow(또는 Block)
- Count가 비종결이라는 점이 "Count로 테스트" 패턴의 핵심 — Block으로 안 끊고 로그만 쌓으며 뒤 규칙도 계속 평가하니 오탐을 안전하게 관찰할 수 있다.
요청 하나가 WAF를 통과하는 전체 흐름 (종합 예시)
POST /login?next=%27+OR+1%3D1-- Host: shop.example
Body: {"user":"admin","pass":"' OR '1'='1"}
1) CloudFront/ALB가 TLS 복호화 + HTTP 재조립 → 완성된 요청
2) [priority 0] IP set(사내) 매칭? → 아니오, 다음
3) [priority 1] Rate-based: 이 IP 5분 내 임계 이하 → 통과
4) [priority 2] SQLi rule:
- 컴포넌트: query 'next' + body 'pass'
- Text transform: URL_DECODE → next = ' OR 1=1--
- SQL 파싱: tautology + 주석 구조 감지 → 매칭!
- Action: Block(종결) → 403 반환, 백엔드로 안 감
5) 로그: 어떤 규칙이 막았는지 Firehose→S3/CloudWatch에 기록
Managed Rules (빈출 — 실무 기본값)
직접 SQLi/XSS 규칙을 짜지 않고, AWS가 관리·업데이트하는 규칙 세트를 붙인다.
| 대표 Managed Rule Group | 막는 것 |
|---|---|
| Core rule set (CRS) | OWASP 일반 위협(광범위 기본기) |
| Known bad inputs | 알려진 악성 페이로드 |
| SQL database | SQL 인젝션 |
| Linux / POSIX / Windows OS | OS 명령어 인젝션 |
| IP reputation (Amazon) | 평판 나쁜 IP·봇넷 |
| Bot Control | 자동화 봇 탐지·관리(스크래퍼, 스캐너) |
| Account Takeover Prevention (ATP) | 크리덴셜 스터핑·계정 탈취 |
| Fraud Control / Account Creation (ACFP) | 가짜 계정 대량 생성 방지 |
시험 트리거: "관리 부담 없이 OWASP Top 10 / SQLi·XSS를 막고 싶다" → AWS Managed Rules. "봇/스크래퍼 차단" → Bot Control.
어디에 붙나 (통합 지점) — 자주 틀림
| 붙일 수 있음 | 못 붙음 |
|---|---|
| CloudFront (글로벌 엣지) | EC2에 직접 |
| Application Load Balancer (ALB) | NLB (L4라서) |
| API Gateway (REST) | 일반 TCP 서비스 |
| AppSync (GraphQL) | |
| Cognito User Pool | |
| App Runner / Verified Access |
핵심 포인트:
- NLB에는 WAF를 못 붙인다. NLB는 L4(TCP/UDP)라서 검사할 HTTP 요청이 없다 → 필요하면 앞단에 ALB나 CloudFront를 두고 거기에 WAF.
- CloudFront에 붙이면 공격을 엣지에서(사용자 가까이) 조기 차단 → 오리진 부하·지연 감소. 글로벌 방어 시 유리.
- ALB에 붙이면 리전 단위. 오리진 직전 방어.
실무 활용 사례
사례 1. OWASP 공격 차단 (SQLi/XSS)
- ALB나 CloudFront에 Web ACL 붙이고 Managed Core rule set + SQL database rule 적용 → 코드 수정 없이 인젝션·XSS 방어.
사례 2. 로그인 브루트포스·스크래핑 방어
- Rate-based rule(IP당 5분 임계치) + Bot Control → 자동화 폭주 IP 차단, 정상 사용자 영향 최소.
사례 3. 지리적 규제·라이선스
- Geo match로 서비스 불가 국가 차단, 또는 특정 국가만 허용(allow-list).
사례 4. 알려진 악성 IP 즉시 차단
- IP set에 넣어 Block. (자동화하려면 IP set을 Lambda/보안 피드로 갱신.)
사례 5. CloudFront + WAF로 엣지 방어
- 정적/동적 사이트를 CloudFront 뒤에 두고 WAF를 CloudFront에 연결 → 오리진 도달 전 엣지에서 L7 공격 차단, 캐시로 성능까지.
사례 6. 로깅·가시성
- Web ACL 로그를 Kinesis Data Firehose → S3 / CloudWatch Logs로 보내 어떤 규칙이 무엇을 막았는지 분석·튜닝.
WAF vs Shield vs Network Firewall vs SG·NACL (핵심 비교)
| 서비스 | 계층 | 무엇을 | 위치 |
|---|---|---|---|
| WAF | L7 | HTTP 요청 내용(SQLi/XSS/봇/rate) | CloudFront/ALB/API GW |
| Shield Standard | L3/L4 | 흔한 볼류메트릭 DDoS(자동·무료) | 엣지(CloudFront/R53/ELB) |
| Shield Advanced | L3/L4(+L7 연동) | 대규모 DDoS + DRT + 요금보호 | 엣지·리전 |
| Network Firewall | L3–L7(VPC) | VPC 전체 트래픽 필터·IDS/IPS | VPC 경계 |
| Security Group | L3/L4 | ENI 단위 stateful allow | 인스턴스 |
| NACL | L3/L4 | 서브넷 stateless allow/deny | 서브넷 |
한 줄 정리: 요청 내용(L7 웹공격) = WAF, 볼류메트릭 DDoS(L3/L4) = Shield, VPC 전반 네트워크 필터·IPS = Network Firewall, IP·포트 접근제어 = SG/NACL.
Common Wrong Directions
| 오답 방향 | 왜 부족한가 |
|---|---|
| SG/NACL로 SQLi·XSS를 막으려 함 | L3/L4라 요청 내용을 못 봄 → WAF |
| L3/L4 볼류메트릭 DDoS를 WAF로 | WAF는 L7 → Shield |
| L7 HTTP Flood를 무조건 Shield로 | 요청 폭주는 WAF Rate-based rule |
| NLB에 WAF 붙이기 | NLB는 L4 → ALB/CloudFront 앞단에 WAF |
| WAF를 EC2에 직접 | 통합 리소스(CloudFront/ALB/API GW)에만 |
| 새 규칙을 바로 Block으로 전개 | 오탐 위험 → 먼저 Count(비종결)로 검증 |
| SQLi 규칙을 직접 다 작성 | AWS Managed Rules로 관리부담 절감 |
| 특정 국가 차단을 앱 코드로 | Geo match가 정답 |
인코딩된 공격(%27, <)이 규칙을 통과 | Text Transformation(URL/HTML decode 등)으로 정규화 후 매칭 |
| SQLi를 키워드 블랙리스트로 막으려 함 | WAF는 SQL 구문 파싱 기반 → 회피·오탐에 강함 |
| 큰 POST 바디 뒤쪽 페이로드가 안 잡힘 | 바디는 앞부분(8/16 KB)만 검사 → 한계 상향·oversize handling 설계 |
Exam Triggers
| 문제에서 이런 표현이 나오면 | 떠올릴 것 |
|---|---|
| "block SQL injection / XSS / OWASP" | WAF (Managed Rules) |
| "filter HTTP requests by content/URI/header" | WAF |
| "one IP sends too many requests / brute force / scraping" | WAF Rate-based rule |
| "block/allow specific countries" | WAF Geo match |
| "block malicious IP list at L7" | WAF IP set |
| "protect against bots / credential stuffing" | WAF Bot Control / ATP |
| "test a rule without impacting traffic" | Count action (비종결) |
| "attackers evade rules with URL/HTML encoding" | Text Transformation |
| "inspect large request bodies fully" | 바디 검사 한계 상향 + oversize handling |
| "large-scale volumetric DDoS (L3/L4)" | Shield (Advanced) |
| "L7 HTTP flood" | WAF Rate-based (+ Shield Advanced) |
| "protect a service behind NLB with WAF" | 앞에 ALB/CloudFront 두고 WAF |
| "filter all VPC traffic / IDS-IPS" | Network Firewall |
| "block attacks at the edge, close to users" | CloudFront + WAF |
Memory Sentence
WAF는 CloudFront/ALB/API GW에 붙어 **HTTP 요청 내용(L7)**을 검사해 SQLi·XSS·봇·지리·rate를 Allow/Block/Count/CAPTCHA로 처리한다. L4(SYN/UDP flood)는 애초에 HTTP 요청이 안 되니 WAF 대상이 아니라 Shield가 막고, L7 요청 폭주는 WAF Rate-based rule로 막는다. 관리부담은 Managed Rules, 오탐 검증은 Count, NLB엔 못 붙으니 앞에 ALB/CloudFront.
References
댓글
아직 댓글이 없습니다.