CSRF·XSS 대비 입력 검증과 출력 인코딩
웹 애플리케이션에서 CSRF와 XSS 위협을 줄이기 위한 입력 검증 원칙, 출력 인코딩 기법, Node·Express 적용 예시 및 라이브러리 활용 방식을 정리한 기술자료
목차
개요
웹 보안에서 CSRF와 XSS는 자주 맞닥뜨리는 위협이다. 두 공격은 목적과 방어 지점이 다르다. 하지만 공통으로 입력 검증과 출력 인코딩이 핵심 방어 수단이 된다. 본문은 처음 접하는 개발자도 이해하기 쉬운 흐름으로 설명한다.
핵심 개념
CSRF(사이트 간 요청 위조)
CSRF는 사용자의 인증 상태를 악용해 의도하지 않은 요청을 서버에 전달시키는 공격이다. 세션·쿠키 기반 인증을 사용하는 서비스에서 특히 위험하다. 방어는 요청의 출처를 검증하거나, 예측 불가능한 토큰을 활용해 요청 무결성을 확인하는 방식으로 이뤄진다.
XSS(크로스 사이트 스크립팅)
XSS는 악성 스크립트를 클라이언트에 주입해 실행시키는 공격이다. 반사형·저장형·DOM 기반 XSS로 구분된다. 방어는 입력에서 위험 문자를 제거하거나, 출력 시 적절히 인코딩해 브라우저가 스크립트를 실행하지 못하도록 만드는 것이 핵심이다.
입력 검증 전략
입력 검증은 신뢰하지 않는 데이터를 안전화하는 첫 단계다. 원칙은 간단하다.
- 허용 목록(Whitelist)을 우선 적용한다. 허용 가능한 형식만 통과시킨다.
- 경계값 검증을 수행한다. 길이, 타입, 패턴을 명확히 정의한다.
- 클라이언트 검증은 보조 수단이다. 항상 서버에서 재검증한다.
Node.js에서의 라이브러리 활용
직접 구현하기보다 검증된 라이브러리를 사용하는 편이 안전하다. 대표적으로 express-validator와 Joi가 있다. express-validator는 Express 미들웨어 형태로 통합이 쉽다. Joi는 스키마 기반으로 복잡한 구조를 검증하기 좋다.
const express = require('express');
const { body, validationResult } = require('express-validator');
const app = express();
app.use(express.json());
app.post('/submit', [
body('email').isEmail().normalizeEmail(),
body('age').optional().isInt({ min: 0, max: 120 })
], (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
res.send('ok');
});
출력 인코딩(Encoding) 원칙
출력 인코딩은 데이터가 어느 컨텍스트로 주입되는지에 따라 달라진다. 대표 컨텍스트는 HTML, 속성(attribute), JavaScript, CSS, URL이다. 각 컨텍스트에 맞는 인코딩을 적용하면 브라우저가 데이터 일부를 코드로 해석하지 못하게 한다.
HTML 컨텍스트 예
HTML 본문에 값을 출력할 때는 HTML 엔티티로 인코딩한다. Node에서 he 같은 라이브러리를 쓰면 안전하다.
const he = require('he');
const userInput = '<script>alert(1)</script>';
const safe = he.encode(userInput);
// safe는 '<script>alert(1)</script>'
JavaScript, 속성, URL 컨텍스트
JavaScript 내에 직접 삽입할 때는 JSON.stringify나 별도 이스케이프를 사용한다. 속성 값은 속성용 이스케이프를 적용한다. URL 파라미터는 encodeURIComponent로 인코딩한다.
// JS 컨텍스트 예
const data = 'value with \"quotes\" and \n newlines';
const safeJs = JSON.stringify(data); // 안전한 JS 문자열 리터럴
// URL 인코딩 예
const param = encodeURIComponent('a+b & c');
Express 환경에서 CSRF 보호
Express 기반 앱에서는 csurf 미들웨어를 사용해 토큰 기반 보호를 적용할 수 있다. 토큰은 폼 필드나 헤더에 담아 전송한다. 쿠키 기반 세션을 쓸 때는 cookie-parser와의 순서도 중요하다.
const express = require('express');
const cookieParser = require('cookie-parser');
const csurf = require('csurf');
const app = express();
app.use(cookieParser());
app.use(express.urlencoded({ extended: true }));
// 쿠키를 이용한 CSRF 토큰
app.use(csurf({ cookie: true }));
app.get('/form', (req, res) => {
res.send('<form method="POST" action="/submit">' +
'<input type="hidden" name="_csrf" value="' + req.csrfToken() + '" />' +
'<input name="data" /></form>');
});
실무 체크리스트
- 모든 입력은 서버에서 재검증한다.
- 허용 목록 방식을 기본으로 한다.
- 출력 시 컨텍스트 별 인코딩을 적용한다.
- Express는 미들웨어 순서에 주의한다. (body-parser → cookie-parser → csurf 등)
- 라이브러리(he, express-validator, csurf 등)를 적극 활용한다.
- 외부 콘텐츠를 포함할 때는 콘텐츠 보안 정책(CSP)을 보조 수단으로 도입한다.
맺음말
입력 검증과 출력 인코딩은 상호 보완적이다. 입력을 통제하면 공격 표면을 줄일 수 있다. 출력 인코딩은 남은 위험을 차단한다. Node 환경에서는 검증된 라이브러리를 적절히 결합하면 실전 수준의 방어를 구축할 수 있다. 마지막으로 정기적인 코드 리뷰와 보안 테스트를 병행해 방어 상태를 확인하는 것이 중요하다.