Rate Limit·CORS·CSP로 API 보안 강화하기
Rate Limit, CORS, CSP의 개념과 Express 적용 예제를 통해 Node.js 기반 API의 취약점을 줄이고 신뢰성 있는 보안 구성
목차
서론: API 보안의 세 가지 축
현대 웹 서비스에서 API는 외부와 연결되는 핵심 접점이다. 따라서 무차별 요청, 악성 도메인 호출, 스크립트 주입 등의 위협을 막기 위한 기본적 방어가 필요하다. 이 글에서는 Rate Limit, CORS, CSP 세 가지를 실무 관점에서 설명한다. 각 기술의 목적과 Express 적용 방법을 단계별로 소개해 처음 접하는 사람도 이해할 수 있도록 구성했다.
Rate Limit(요청 제한)
왜 필요한가
Rate Limit은 동일 출처 또는 동일 IP에서 오는 요청 빈도를 제한한다. DDoS나 무차별 로그인 시도를 완화하고, 리소스 고갈을 방지해 서비스 가용성을 지킨다. 단순히 차단만 하는 것이 아니라 유연한 정책으로 정상 사용자 경험을 유지해야 한다.
어떤 전략이 있는가
- 고정 윈도우(Fixed Window): 특정 기간 내 요청 횟수를 제한한다.
- 슬라이딩 윈도우(Sliding Window): 시간 창을 이동하며 보다 균등하게 제한한다.
- 토큰 버킷(Token Bucket): 일정량의 토큰을 소비하는 방식으로 버스트 트래픽을 허용한다.
Express 예제
간단한 Express 적용 예제를 통해 Rate Limit을 설정하는 방법을 본다. npm 패키지로 구현하거나 리버스 프록시(Nginx, Cloudflare)에서 처리할 수 있다. 아래는 Express에서 인기 있는 미들웨어 사용 예시다.
const express = require('express')
const rateLimit = require('express-rate-limit')
const app = express()
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
standardHeaders: true, // Return rate limit info in the RateLimit-* headers
legacyHeaders: false, // Disable the X-RateLimit-* headers
})
// Apply to all requests
app.use(limiter)
app.get('/', (req, res) => {
res.send('Hello, world')
})
app.listen(3000)
위 설정은 15분당 100회로 제한한다. 필요한 경우 로그인 엔드포인트에 더 엄격한 정책을 적용하거나, API 키별 차등 정책을 추가할 수 있다.
CORS(교차 출처 자원 공유)
핵심 개념
CORS는 브라우저가 다른 출처의 자원에 접근할 때 보안 제약을 적용하는 표준이다. 서버는 응답 헤더로 허용되는 출처, 메서드, 헤더 등을 명시한다. 잘못 설정하면 합법적 호출이 차단되거나, 반대로 악성 도메인에 API를 노출할 수 있다.
Node.js CORS 설정 방법
Express에서는 cors 미들웨어로 간단히 설정할 수 있다. 신뢰된 출처만 허용하고, 필요할 때에만 자격증명(쿠키, 인증 헤더)을 허용하는 것이 안전하다. 아래 예시에서 출처 화이트리스트와 동적 허용을 보여준다.
const cors = require('cors')
const whitelist = ['https://example.com', 'https://admin.example.com']
const corsOptions = {
origin: function (origin, callback) {
if (!origin || whitelist.indexOf(origin) !== -1) {
callback(null, true)
} else {
callback(new Error('Not allowed by CORS'))
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true,
}
app.use(cors(corsOptions))
위 코드는 신뢰된 도메인만 허용한다. 테스트 도메인을 제외한 요청은 거부된다. API 인증 방식이 토큰 기반이면 credentials를 false로 유지해도 무방하다.
주의할 점
- Access-Control-Allow-Origin: '*'은 인증 정보가 있는 요청과 혼용하면 위험하다.
- 사전 요청(preflight)과 실제 요청의 헤더를 모두 검토해야 한다.
- 동적 허용 시 에러 처리를 통해 불필요한 정보 노출을 피한다.
CSP(Content Security Policy)
무엇을 막는가
CSP는 브라우저가 로드할 수 있는 리소스의 출처를 제한한다. 스크립트 인젝션, 스타일 인젝션, 프레임 삽입 등 클라이언트 측 공격을 크게 줄인다. 서버는 응답 헤더로 정책을 전달한다.
Content Security Policy Node 적용
Node 환경에서는 Helmet 같은 라이브러리로 CSP를 설정할 수 있다. 정책은 엄격할수록 안전하지만, 외부 CDN이나 인라인 스크립트 사용 여부에 따라 조정이 필요하다. 아래는 기본적인 Helmet 사용 예시이다.
const helmet = require('helmet')
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'https://cdn.example.com'],
styleSrc: ["'self'", 'https://fonts.example.com'],
imgSrc: ["'self'", 'data:'],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
}
}))
만약 인라인 스크립트를 반드시 사용해야 하면 nonce 기반 정책을 적용해 위험을 완화한다. CSP 설정은 개발 단계에서 모니터링 모드(report-only)로 충분히 검증한 뒤 강화해야 한다.
조합 전략과 운영 팁
세 기술은 상호 보완적으로 작동한다. Rate Limit은 리소스 남용을 막고, CORS는 불법 도메인 호출을 제어하며, CSP는 브라우저에서의 스크립트 공격을 차단한다. 운영 시 고려사항은 다음과 같다.
- 우선순위: 인증·인가 로직과 함께 Rate Limit을 우선 적용해 서비스 보호.
- 환경별 정책: 개발 환경에서는 완화된 정책을 사용하고, 스테이징·프로덕션에서 강화.
- 모니터링: 실패 로그, 브라우저의 CSP 리포트, Rate Limit 초과율을 수집해 정책을 조정.
- 테스트: 다양한 클라이언트(모바일, SPA, 외부 서비스)에서 정책 영향을 확인.
마무리
Rate Limit, CORS, CSP는 각각 다른 층에서 보안을 강화한다. Node.js와 Express 환경에서는 간단한 미들웨어 조합만으로도 상당한 수준의 방어가 가능하다. 정책은 서비스 특성에 맞춰 세밀하게 조정하고, 모니터링을 통해 점진적으로 강화하는 것이 실용적인 접근이다.