
웹 서비스를 외부에 공개하면 X-Forwarded-For 스푸핑, XXE, SSRF, XSS, CSRF, API 권한 검증 실패처럼 내부 시스템에서는 드러나지 않던 웹 해킹 위험이 실제 침해 사고로 이어질 수 있습니다.
이 글에서는 개발자가 반드시 이해해야 할 주요 웹 공격의 원리와 함께, 리버스 프록시·백엔드·API·브라우저·파일 처리 구간에서 적용할 수 있는 구체적인 보안 대응 방법을 정리합니다.
내부 시스템에서 외부 공개형(Public) 서비스가 되면 무엇이 달라지는가
기업 내부망에서만 사용하던 솔루션은 공격 표면이 상대적으로 제한적입니다. 사용자는 임직원이나 협력사로 한정되고, 네트워크 접근 자체가 인증 수단처럼 작동하기도 합니다.
그래서 입력값 검증, 권한 검증, 프록시 헤더 처리, API 호출 제한이 다소 느슨해도 실제 사고로 이어지지 않는 경우가 있었습니다.
그러나 서비스를 인터넷에 공개하는 순간 전제가 바뀝니다. 모든 요청은 자동화될 수 있고, 클라이언트가 보내는 헤더·본문·파일 이름·콘텐츠 타입·URL·객체 ID는 모두 조작될 수 있습니다.
정상 사용자는 UI가 제공하는 순서대로 기능을 사용하지만, 공격자는 UI를 거치지 않고 API를 직접 호출합니다. 한 번의 요청이 아니라 수천 개의 변형 요청을 빠르게 반복하며 경계 조건을 찾습니다.
2025년 OWASP Top 10은 Broken Access Control, Security Misconfiguration, Software Supply Chain Failures, Injection, Insecure Design, Authentication Failures, Logging & Alerting Failures 등을 핵심 위험으로 분류합니다.
여기서 중요한 점은 특정 취약점 하나만 막는 것으로 끝나지 않는다는 것입니다. 접근 통제, 설정, 설계, 인증, 로깅이 서로 보완하지 않으면 하나의 결함이 다른 결함을 증폭시킵니다.
핵심 개념, 신뢰 경계와 심층 방어
웹 보안 문제의 상당수는 기술 이름이 아니라 신뢰 경계의 오류로 설명할 수 있습니다. 서버가 신뢰하면 안 되는 값을 신뢰하거나, 신뢰해야 할 주체를 확인하지 않거나, 한 계층의 방어가 다른 계층에도 동일하게 적용될 것이라고 가정할 때 문제가 발생합니다.
클라이언트 입력 경계
헤더, 쿠키, JSON, XML, URL, 파일, 객체 ID, 정렬 조건은 전부 공격자 제어 입력으로 취급합니다.
프록시·애플리케이션 경계
XFF, Host, Proto 같은 전달 헤더는 신뢰 프록시가 재작성한 경우에만 사용할 수 있습니다.
애플리케이션·내부망 경계
서버가 외부 URL을 호출할 수 있다면 SSRF를 통해 내부 API와 메타데이터 서비스가 노출될 수 있습니다.
사용자·객체 권한 경계
로그인 여부와 특정 객체에 대한 접근 권한은 별개의 검증입니다. 요청마다 객체 단위 권한을 확인해야 합니다.
제로 트러스트(Zero Trust)와 심층 방어(Defense in Depth)
제로 트러스트(Zero Trust)는 “내부망이므로 신뢰한다”는 가정을 제거합니다. 요청 주체, 대상 리소스, 행위, 환경을 매번 검증합니다.
심층 방어(Defense in Depth)는 하나의 방어가 실패해도 다음 계층이 피해 확산을 차단하도록 만드는 설계입니다.
X-Forwarded-For 파싱 취약점과 IP 스푸핑
X-Forwarded-For, 줄여서 XFF는 리버스 프록시나 로드 밸런서 뒤에 있는 애플리케이션이 원래 클라이언트 IP를 알 수 있도록 전달하는 비표준 헤더입니다. IETF는 표준 형태로 Forwarded 헤더를 정의했지만, 실제 환경에서는 XFF가 광범위하게 사용됩니다.
클라이언트는 XFF를 임의로 보낼 수 있습니다. 따라서 애플리케이션이 XFF의 첫 번째 값을 무조건 실제 IP로 사용하면 요청 제한(Rate Limit), IP 허용 목록, 관리자 접근 제한, 로그인 이상 탐지, 감사 로그가 모두 우회되거나 오염될 수 있습니다.
왜 “첫 번째 IP”가 항상 실제 사용자 IP가 아닌가
일반적인 XFF 체인은 왼쪽에 최초 클라이언트, 오른쪽에 요청을 전달한 프록시가 추가되는 형태입니다. 하지만 최초 값이 공격자가 직접 삽입한 값인지, 신뢰할 수 있는 프록시가 기록한 값인지 헤더 문자열만으로는 알 수 없습니다.
RFC 7239 역시 전달 헤더는 클라이언트를 포함한 경로상의 노드가 수정할 수 있으므로 정확성을 전제로 해서는 안 된다고 명시합니다.
안전한 처리 원칙
- 인터넷과 맞닿은 에지(Edge) 프록시에서 외부가 보낸 XFF·Forwarded 계열 헤더를 제거하거나 덮어씁니다.
- 애플리케이션까지 요청을 전달할 수 있는 프록시 IP 또는 CIDR을 명시적으로 관리합니다.
- 오른쪽부터 신뢰 프록시를 제거하고, 처음 만나는 비신뢰 주소를 클라이언트 주소로 판단합니다.
- 원본 소켓 IP와 파생된 클라이언트 IP를 모두 기록해 사고 분석이 가능하도록 합니다.
- IP는 보조 신호로만 사용하고, 핵심 권한 판단을 IP 하나에 의존하지 않습니다.
NGINX 방어 설정 예시
# 실제 운영 환경의 프록시 주소로 교체해야 합니다.
set_real_ip_from 203.0.113.10;
set_real_ip_from 198.51.100.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# 로그에는 원본 연결 주소와 재계산된 주소를 함께 남깁니다.
log_format secure_main
'$remote_addr original=$realip_remote_addr '
'xff="$http_x_forwarded_for" request="$request" status=$status';
Express 방어 설정 예시
const secure_express = require("express");
const secure_app = secure_express();
const secure_trustedProxies = [
"203.0.113.10",
"198.51.100.0/24"
];
secure_app.set("trust proxy", secure_trustedProxies);
function secure_writeRequestLog(secure_request, secure_response, secure_next) {
const secure_logRecord = {
secure_socketIp: secure_request.socket.remoteAddress,
secure_derivedIp: secure_request.ip,
secure_xff: secure_request.get("x-forwarded-for") || ""
};
console.log(JSON.stringify(secure_logRecord));
secure_next();
}
secure_app.use(secure_writeRequestLog);
Express에서 trust proxy = true 또는 단순 hop count만 사용하면 배포 경로가 변경되었을 때 신뢰 범위가 예상보다 넓어질 수 있습니다. 프록시 주소 또는 네트워크 범위를 명시하는 방식이 더 안전합니다.
XML 업로드, XXE와 내부 API 정보 탈취
사용자가 업로드한 XML, SVG, SOAP 메시지, SAML 문서 등을 서버가 파싱할 때 외부 엔티티 또는 외부 DTD를 허용하면 XXE(XML External Entity) 취약점이 발생할 수 있습니다.
XXE의 본질은 XML 문서 안의 참조를 파서가 로컬 파일이나 외부 네트워크 리소스로 해석하는 데 있습니다.
공격 흐름을 방어 관점에서 이해하기
이 과정은 SSRF로 확장될 수 있습니다. 외부 사용자가 직접 접근할 수 없는 내부 API라도, 인터넷과 내부망 모두에 접근할 수 있는 애플리케이션 서버가 대신 요청을 보내면 접근 경계가 무너집니다.
클라우드 환경에서는 인스턴스 메타데이터, 관리 API, 서비스 디스커버리 주소가 특히 민감합니다.
안전한 방어 원칙
- 가능하면 XML 대신 JSON 등 외부 참조 기능이 없는 포맷을 사용합니다.
- DOCTYPE 선언, 외부 일반 엔티티, 외부 파라미터 엔티티, 외부 DTD 로드를 비활성화합니다.
- XInclude와 외부 스키마 조회를 비활성화하거나 명시적 허용 목록으로 제한합니다.
- XML 처리 프로세스의 인터넷과 내부망으로 향하는 외부 통신(Egress)을 네트워크 수준에서 제한합니다.
- 업로드 파일의 확장자만 보지 말고 실제 MIME, 매직 바이트, 구조, 크기를 함께 검증합니다.
- 파서 라이브러리 버전을 고정하지 말고 보안 업데이트와 기본값 변경을 지속적으로 추적합니다.
Java JAXP 안전 설정 예시
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
public final class secure_XmlParserFactory {
private secure_XmlParserFactory() {
}
public static DocumentBuilderFactory secure_createFactory() throws Exception {
DocumentBuilderFactory secure_factory =
DocumentBuilderFactory.newInstance();
secure_factory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl",
true
);
secure_factory.setFeature(
"http://xml.org/sax/features/external-general-entities",
false
);
secure_factory.setFeature(
"http://xml.org/sax/features/external-parameter-entities",
false
);
secure_factory.setAttribute(
XMLConstants.ACCESS_EXTERNAL_DTD,
""
);
secure_factory.setAttribute(
XMLConstants.ACCESS_EXTERNAL_SCHEMA,
""
);
secure_factory.setXIncludeAware(false);
secure_factory.setExpandEntityReferences(false);
return secure_factory;
}
}
Python lxml 안전 설정 예시
from lxml import etree
def secure_parseXml(secure_xmlBytes: bytes):
secure_parser = etree.XMLParser(
resolve_entities=False,
load_dtd=False,
no_network=True,
huge_tree=False
)
return etree.fromstring(
secure_xmlBytes,
parser=secure_parser
)
“XML 파일인지 확인했다”는 것은 방어가 아닙니다. XXE는 파일 확장자 문제가 아니라 파서의 외부 참조 처리 문제입니다.
애플리케이션 설정과 네트워크 외부 통신(Egress) 제한을 동시에 적용해야 합니다.
스푸핑과 헤더 신뢰 문제
스푸핑은 공격자가 자신을 다른 주체처럼 보이게 만드는 행위를 의미합니다. 웹 서비스에서는 단순한 IP 위조뿐만 아니라 Host 헤더, 전달 헤더, 이메일 발신 도메인, DNS 응답, 웹훅(Webhook) 발신자, 사용자 에이전트 등 다양한 형태로 나타납니다.
| 유형 | 잘못된 신뢰 | 가능한 영향 | 핵심 방어 |
|---|---|---|---|
| XFF/IP 스푸핑 | 클라이언트 제공 헤더를 실제 IP로 신뢰 | 접근 제어·요청 제한(Rate Limit)·감사 로그 우회 | 신뢰 프록시 목록과 에지 프록시 재작성 |
| Host 헤더 인젝션 | Host 값을 절대 URL 생성에 무검증 사용 | 비밀번호 재설정 링크 오염, 캐시 문제 | 허용 호스트 목록 고정, 외부 URL 설정값 사용 |
| 웹훅(Webhook) 스푸핑 | 요청이 도착했다는 사실만 정상 발신자의 증거로 간주 | 주문·결제·배포 상태 위조 | HMAC 서명, 타임스탬프, 재전송 방지 |
| 이메일 스푸핑 | 표시 이름 또는 From 주소만 신뢰 | 피싱, 계정 탈취, BEC | SPF·DKIM·DMARC와 업무 절차 검증 |
Host 헤더를 이용한 URL 생성 금지
비밀번호 재설정 링크, OAuth Redirect URI, 이메일 인증 링크를 만들 때 요청의 Host 헤더를 그대로 사용하면 공격자가 외부 도메인이 포함된 링크를 생성하도록 유도할 수 있습니다.
서비스의 외부 기본 URL은 환경 설정에 고정하고, 요청 호스트는 허용 목록으로 검증해야 합니다.
const secure_publicBaseUrl =
process.env.SECURE_PUBLIC_BASE_URL;
if (!secure_publicBaseUrl) {
throw new Error("SECURE_PUBLIC_BASE_URL is required");
}
function secure_buildPasswordResetUrl(secure_token) {
const secure_url = new URL(
"/account/reset-password",
secure_publicBaseUrl
);
secure_url.searchParams.set("token", secure_token);
return secure_url.toString();
}
웹훅(Webhook) 서명 검증 예시
const secure_crypto = require("node:crypto");
function secure_verifyWebhook(
secure_rawBody,
secure_receivedSignature,
secure_secret
) {
const secure_expectedSignature = secure_crypto
.createHmac("sha256", secure_secret)
.update(secure_rawBody)
.digest("hex");
const secure_expectedBuffer =
Buffer.from(secure_expectedSignature, "utf8");
const secure_receivedBuffer =
Buffer.from(secure_receivedSignature, "utf8");
if (secure_expectedBuffer.length !== secure_receivedBuffer.length) {
return false;
}
return secure_crypto.timingSafeEqual(
secure_expectedBuffer,
secure_receivedBuffer
);
}
XSS : 브라우저에서 공격자 입력이 코드가 되는 순간
XSS(Cross-Site Scripting)는 공격자 입력이 다른 사용자의 브라우저에서 실행 가능한 코드로 해석되는 취약점입니다. 저장형, 반사형, DOM 기반으로 구분할 수 있지만 공통 원인은 신뢰할 수 없는 데이터를 실행 가능한 HTML·JavaScript 문맥에 넣는 것입니다.
저장형 XSS
게시글, 닉네임, 관리자 메모처럼 저장된 값이 이후 다른 사용자 화면에서 실행됩니다.
반사형 XSS
검색어, 오류 메시지, URL 파라미터가 응답 HTML에 즉시 반영됩니다.
DOM XSS
클라이언트 JavaScript가 URL이나 외부 데이터를 위험한 DOM Sink에 전달합니다.
CSP
스크립트 실행 출처를 제한해 피해를 줄이지만, 출력 인코딩과 새니타이징(Sanitization)을 대체하지는 못합니다.
방어 원칙
- HTML, 속성, JavaScript, URL, CSS 등 출력 문맥에 맞는 인코딩을 적용합니다.
- 단순 문자열은 innerHTML이 아니라 textContent로 출력합니다.
- 리치 텍스트가 필요하면 검증된 새니타이저(Sanitizer)와 좁은 태그·속성 허용 목록를 사용합니다.
- 프레임워크의 자동 이스케이프를 끄는 API는 코드 리뷰 필수 대상으로 지정합니다.
- Nonce 또는 Hash 기반 CSP를 단계적으로 적용합니다.
- 세션 쿠키에 HttpOnly를 부여해 JavaScript 접근을 제한합니다.
function secure_renderText(
secure_targetElement,
secure_untrustedText
) {
secure_targetElement.textContent =
secure_untrustedText;
}
CSP 예시
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{secure_nonce}}';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
CSP는 실행 경로를 제한하는 심층 방어입니다. 잘못된 DOM 조작, 과도한 외부 스크립트 허용, unsafe-inline 사용, 취약한 서드 파티 스크립트가 있으면 방어력이 크게 약화됩니다.
CSRF : 사용자의 인증 상태를 이용한 의도하지 않은 요청
CSRF(Cross-Site Request Forgery)는 사용자가 로그인한 브라우저가 인증 쿠키를 자동으로 전송한다는 점을 악용해, 사용자가 의도하지 않은 상태 변경 요청을 보내도록 만드는 공격입니다.
계정 정보 변경, 결제 수단 등록, 관리자 설정 변경처럼 쿠키 인증을 사용하는 기능이 주요 대상입니다.
안전한 상태 변경 요청 조건
- GET 요청은 조회에만 사용하고 상태를 변경하지 않습니다.
- POST·PUT·PATCH·DELETE에 CSRF 토큰을 검증합니다.
- Origin 또는 Referer를 검증합니다.
- Sec-Fetch-Site 등 Fetch Metadata를 보조 방어로 사용합니다.
- 세션 쿠키에 목적에 맞는 SameSite 속성을 지정합니다.
- 이메일·비밀번호·MFA·출금 등 고위험 작업은 재인증을 요구합니다.
function secure_validateStateChangingRequest(
secure_request,
secure_response,
secure_next
) {
const secure_stateChangingMethods =
new Set(["POST", "PUT", "PATCH", "DELETE"]);
if (!secure_stateChangingMethods.has(secure_request.method)) {
return secure_next();
}
const secure_origin =
secure_request.get("origin") || "";
const secure_fetchSite =
secure_request.get("sec-fetch-site") || "";
const secure_csrfToken =
secure_request.get("x-csrf-token") || "";
if (secure_fetchSite === "cross-site") {
return secure_response.status(403).send("Forbidden");
}
if (secure_origin !== "https://app.example.com") {
return secure_response.status(403).send("Forbidden");
}
if (
!secure_csrfToken ||
secure_csrfToken !== secure_request.session.secure_csrfToken
) {
return secure_response.status(403).send("Forbidden");
}
return secure_next();
}
XSS가 존재하면 페이지 내부에서 정상 CSRF 토큰을 읽거나 정상 요청 흐름을 호출할 수 있으므로, XSS와 CSRF는 서로 독립된 문제가 아닙니다. 출력 보안과 요청 의도 검증을 함께 적용해야 합니다.
SQL Injection, Command Injection, Path Traversal, 파일 업로드
SQL Injection
SQL Injection은 사용자 입력이 쿼리 구조에 결합될 때 발생합니다. 문자열 이스케이프를 직접 구현하는 방식은 DB 문법, 문자 집합, 타입 변환에 따라 실패할 수 있습니다.
Prepared Statement 또는 파라미터 바인딩을 기본값으로 사용해야 합니다.
async function secure_findUserById(
secure_database,
secure_userId
) {
const secure_query =
"SELECT user_id, user_name FROM users WHERE user_id = $1";
const secure_result = await secure_database.query(
secure_query,
[secure_userId]
);
return secure_result.rows[0] || null;
}
Command Injection
OS 명령 문자열에 사용자 입력을 결합하면 명령 구분자와 옵션 해석을 통해 의도하지 않은 명령이 실행될 수 있습니다. 가능하면 OS 명령 호출을 제거하고 라이브러리 API를 사용합니다.
불가피한 경우 셸을 거치지 않는 실행 API, 고정 명령, 엄격한 인자 허용 목록를 사용합니다.
Path Traversal
다운로드 경로나 압축 해제 대상 경로에 사용자 입력을 직접 결합하면 허용 디렉터리 밖의 파일에 접근할 수 있습니다. 정규화된 경로(Canonical Path)를 계산한 뒤 기준 디렉터리 내부인지 확인해야 하며, 사용자가 실제 저장 경로를 지정하지 못하게 해야 합니다.
const secure_path = require("node:path");
function secure_resolveDownloadPath(
secure_baseDirectory,
secure_serverFileName
) {
const secure_basePath =
secure_path.resolve(secure_baseDirectory);
const secure_targetPath =
secure_path.resolve(
secure_baseDirectory,
secure_serverFileName
);
if (
!secure_targetPath.startsWith(
secure_basePath + secure_path.sep
)
) {
throw new Error("Invalid path");
}
return secure_targetPath;
}
파일 업로드
파일 업로드 보안은 확장자 검사 하나로 해결되지 않습니다. 파일 이름, 확장자, MIME, 실제 내용, 파서 취약점, 저장 위치, 공개 방식, 실행 권한, 이미지 후처리까지 모두 고려해야 합니다.
- 업로드 가능한 파일 유형을 최소 허용 목록으로 제한합니다.
- 클라이언트가 전송한 파일 이름을 저장 파일 이름으로 사용하지 않고 서버에서 무작위 이름을 생성합니다.
- 업로드 디렉터리를 웹 루트와 분리하고 실행 권한을 제거합니다.
- 응답 시 Content-Type과 Content-Disposition을 서버가 결정합니다.
- X-Content-Type-Options : nosniff를 적용합니다.
- 이미지·문서는 안전한 라이브러리로 재인코딩하거나 샌드박스에서 처리합니다.
- 압축 파일은 압축 해제 크기, 파일 수, 경로, 심볼릭 링크를 제한합니다.
- 바이러스 검사만으로 안전하다고 판단하지 않습니다.
API 보안, BOLA, 대량 할당(Mass Assignment), JWT, CORS, 요청 제한(Rate Limit)
OWASP API Security Top 10 : 2023은 객체 단위 권한 검증 실패(BOLA), 인증 실패, 객체 속성 단위 권한 검증 실패, 자원 사용 제한 부재, 기능 단위 권한 검증 실패, 민감한 비즈니스 흐름의 무제한 접근, SSRF, 보안 설정 오류, API 인벤토리 부실, 외부 API의 불안전한 사용을 주요 위험으로 분류합니다.
BOLA·IDOR : 객체 ID를 바꿨을 때 다른 사용자의 데이터가 보이는가
URL 또는 JSON에 포함된 객체 ID는 권한 증명이 아닙니다. 로그인한 사용자가 해당 객체를 조회·수정·삭제할 수 있는지 서버가 요청마다 검증해야 합니다.
async function secure_getOrder(
secure_database,
secure_authenticatedUserId,
secure_orderId
) {
const secure_query = `
SELECT order_id, order_status, total_amount
FROM orders
WHERE order_id = $1
AND owner_user_id = $2
`;
const secure_result = await secure_database.query(
secure_query,
[secure_orderId, secure_authenticatedUserId]
);
return secure_result.rows[0] || null;
}
대량 할당(Mass Assignment)
요청 JSON 전체를 ORM 엔티티에 자동 바인딩하면 사용자가 역할, 승인 상태, 소유자 ID, 할인율처럼 화면에는 없는 필드를 전송해 내부 속성을 변경할 수 있습니다. 입력 DTO를 별도로 만들고 허용 필드만 명시적으로 매핑해야 합니다.
function secure_buildProfileUpdate(
secure_requestBody
) {
return {
secure_displayName:
String(secure_requestBody.secure_displayName || "").trim(),
secure_bio:
String(secure_requestBody.secure_bio || "").trim()
};
}
JWT 보안 실수
- 서명 알고리즘을 서버에서 고정하고 토큰 헤더가 임의로 선택하게 두지 않습니다.
- iss, aud, exp, nbf를 검증합니다.
- 액세스 토큰은 짧게 유지하고 리프레시 토큰은 회전·재사용 탐지를 적용합니다.
- JWT 페이로드는 암호화가 아니므로 민감 정보를 넣지 않습니다.
- 로그아웃·계정 정지·권한 변경 시 토큰 폐기 전략을 설계합니다.
- 키 ID를 이용해 파일 경로나 임의 URL에서 키를 가져오지 않습니다.
CORS 오용
CORS는 인증이나 서버 간 접근 통제가 아니라 브라우저의 교차 출처 응답 읽기 정책입니다.
요청 출처(Origin)을 그대로 반사하면서 자격 증명(Credentials)을 허용하거나, 민감 API에 과도한 Origin을 허용하면 다른 사이트가 사용자의 인증 상태로 API 응답을 읽을 수 있습니다.
- 허용 Origin을 정확한 스킴(Scheme)·호스트(Host)·포트(Port) 단위로 관리합니다.
- 자격 증명(Credentials)을 사용할 때 와일드카드 Origin을 사용하지 않습니다.
- 정규식 허용 규칙은 서브도메인 경계와 문자열 끝을 엄격히 검증합니다.
- CORS가 없어도 서버 측 인증·권한 검증은 항상 수행합니다.
- 단순 요청과 프리플라이트(Preflight) 요청을 구분해 허용 메서드·헤더를 최소화합니다.
요청 제한(Rate Limit)과 자원 제한
IP 하나만 기준으로 제한하면 XFF 오용, NAT, 프록시, 분산 요청에 취약합니다. 사용자·세션·API 키·디바이스·IP·기능 비용을 조합하고, 로그인·인증 코드·검색·파일 변환·AI 호출·결제 같은 고비용 기능은 별도 정책을 적용해야 합니다.
HTTP 요청 스머글링(Request Smuggling)과 Cache Poisoning
HTTP 요청 스머글링(Request Smuggling)
프런트 프록시와 백엔드 서버가 요청의 끝을 서로 다르게 해석하면, 하나의 연결 안에서 요청 경계가 어긋날 수 있습니다. 주로 Content-Length와 Transfer-Encoding 처리 차이, 중복 헤더, 비표준 문법 허용이 원인이 됩니다.
- 프록시와 백엔드의 HTTP 파서 및 프로토콜 설정을 일관되게 유지합니다.
- 중복되거나 모순되는 길이 헤더를 에지(Edge)에서 거부합니다.
- 지원 종료된 프록시·웹 서버·애플리케이션 서버를 사용하지 않습니다.
- 가능하면 프록시와 백엔드 구간을 HTTP/2 또는 명확히 통제된 프로토콜로 표준화합니다.
- 비정상 4xx, 연결 재사용 이상, 다른 사용자 응답 혼입 징후를 모니터링합니다.
웹 캐시 오염(Web Cache Poisoning)
캐시 키에 포함되지 않는 헤더나 파라미터가 응답 생성에는 영향을 주면, 공격자가 변조된 응답을 캐시에 저장해 다른 사용자에게 전달되도록 만들 수 있습니다.
Host 계열 헤더, X-Forwarded-Host, 경로 정규화, 쿼리 문자열, 콘텐츠 협상 헤더를 특히 주의해야 합니다.
- 응답에 영향을 주는 입력은 캐시 키에도 반영하거나 해당 응답을 캐시하지 않습니다.
- 인증 응답과 사용자별 응답은 공용 캐시(공용 캐시(Public Cache)) 대상에서 제외합니다.
- 비표준 전달 헤더를 애플리케이션까지 전달하지 않습니다.
- Cache-Control, Vary, CDN 규칙을 코드와 인프라 양쪽에서 검토합니다.
외부 공개형(Public) 서비스 전환 실무 체크리스트
| 영역 | 필수 점검 | 실패 시 대표 위험 |
|---|---|---|
| Edge·Proxy | 신뢰 프록시, XFF 재작성, 호스트 허용 목록, 요청 크기·시간 제한 | IP 위조, 링크 오염, 우회 호출 |
| 인증·세션 | 세션 회전, Secure·HttpOnly·SameSite, 재인증, 토큰 폐기 | 세션 고정, 계정 탈취 |
| 권한 | 객체·필드·기능 단위 권한 검증 | BOLA, IDOR, 권한 상승 |
| 입력·출력 | 스키마 검증, 파라미터 바인딩, 문맥별 인코딩, 새니타이징(Sanitization) | Injection, XSS |
| 파일·XML | DTD·외부 엔티티 차단, 파일 재명명, 웹 루트 분리, 재인코딩 | XXE, SSRF, 악성 파일 실행 |
| 외부 호출 | URL 허용 목록, DNS·IP 재검증, Redirect 제한, 외부 통신 방화벽(Egress Firewall) | SSRF, 내부망 접근 |
| API | 요청 제한(Rate Limit), 요청·응답 필드 최소화, API 인벤토리, 버전 폐기 | 자원 고갈, 섀도 API(Shadow API) |
| 브라우저 | CSP, CSRF 토큰, Origin 검증, CORS 최소 허용 | XSS, CSRF, 데이터 노출 |
| 의존성 | SBOM, SCA, 잠금 파일, 서명·출처 검증, 신속한 패치 | 공급망 침해 |
| 탐지·대응 | 감사 로그, 상관관계 ID, 경보 기준, 비상 폐기·차단 절차 | 침해 장기화, 원인 분석 실패 |
개발 프로세스에 넣어야 할 자동화
- PR 단계 : Secret Scan, SAST, Dependency Scan, IaC Scan
- 빌드 단계 : SBOM 생성, 아티팩트 서명, 취약 의존성 정책 검사
- 테스트 단계 : 인증·권한 부정 테스트(Negative Test), API 스키마 퍼징(Schema Fuzzing), DAST
- 배포 단계 : 보안 헤더, CORS, 프록시 신뢰 설정, 공개 포트 검증
- 운영 단계 : WAF 로그, 애플리케이션 감사 로그, 클라우드 네트워크 로그 연계
외부 공개형(Public) 서비스로 전환하기 직전이라면 ① 인증·객체 권한, ② 프록시 헤더 신뢰, ③ XSS·CSRF, ④ SSRF·파일·XML, ⑤ 요청 제한(Rate Limit)·자원 제한, ⑥ 로깅·대응 순으로 점검하는 것이 실무적으로 효율적입니다.
다만 개인정보·결제·관리자 기능이 있는 서비스는 해당 기능의 위협 모델을 별도로 작성해야 합니다.
보안은 막아 놓는 기능이 아니라 계속 검증하는 운영 체계다
웹 보안은 흔히 창과 방패의 싸움으로 표현됩니다. 이 표현은 공격 기법이 계속 변한다는 점에서는 맞지만, 방어자가 매번 새로운 공격 이름을 외워야만 한다는 의미는 아닙니다.
대부분의 공격은 신뢰하지 말아야 할 입력을 신뢰하고, 요청 주체와 권한을 충분히 검증하지 않으며, 한 계층의 방어에 과도하게 의존할 때 성립합니다.
내부 시스템에서 외부 공개형(Public) 서비스로 전환할 때 가장 먼저 바꿔야 하는 것은 코드 몇 줄보다 보안 전제입니다.
클라이언트가 보내는 모든 값은 조작될 수 있고, 내부 API도 인증이 필요하며, 로그인한 사용자도 모든 객체에 접근할 수 없고, 프록시와 라이브러리의 기본값도 항상 안전하지는 않습니다.
따라서 방어의 목표는 “완벽하게 뚫리지 않는 시스템”이 아니라 공격 표면을 줄이고, 권한을 최소화하며, 계층별 방어를 겹치고, 변경이 발생할 때마다 자동으로 검증하는 시스템을 만드는 것입니다.
이것이 외부 공개형(Public) 서비스를 운영하는 개발자가 보안을 지속적으로 다뤄야 하는 이유입니다.
검증에 사용한 주요 공식 자료
- OWASP Top 10 : 2025
- OWASP API Security Top 10 : 2023
- IETF RFC 7239 : Forwarded HTTP Extension
- Express : Behind proxies
- NGINX ngx_http_realip_module
- OWASP XML External Entity Prevention Cheat Sheet
- OWASP SSRF Prevention Cheat Sheet
- OWASP Cross Site Scripting Prevention Cheat Sheet
- OWASP CSRF Prevention Cheat Sheet
- OWASP File Upload Cheat Sheet
- OWASP REST Security Cheat Sheet
- MDN : Cross-Origin Resource Sharing
- MDN : Content-Security-Policy
