웹 보안 기초 (HTTPS, XSS, CSRF)
HTTPS의 전송 보호와 XSS·SQL 삽입·CSRF·무차별 대입·DDoS의 공격 경로 및 기본 방어를 구분합니다.
우리는 지금까지 프론트엔드와 백엔드의 역할을 살펴보고, 요청과 응답, 메모리 배열을 사용하는 기본 CRUD 흐름을 익혔습니다.
데이터베이스에서는 저장소의 필요성과 SQL·NoSQL의 기초를 비교했으며, 실제 연결 구현까지 진행한 것은 아닙니다.
하지만 서비스 단계에서는 기능만으로 충분하지 않습니다.
반드시 함께 챙겨야 하는 축이 보안(Security)과 성능(Performance)입니다.
개인정보 유출, 계정 탈취, 서비스 마비 같은 사고는 기능 완성도와 무관하게 신뢰를 한 번에 무너뜨립니다.
반대로 보안과 성능을 초기에 기준으로 잡으면 서비스 운영 비용과 장애 대응 부담을 크게 줄일 수 있습니다.
13장 웹 보안과 성능 최적화의 첫 절에서는 웹 보안의 기본 개념, 자주 발생하는 공격 유형, 그리고 실제 개발에서 바로 적용할 수 있는 방어 전략을 다룹니다.
웹 보안은 웹 애플리케이션과 사용자의 데이터를 악의적인 공격으로부터 보호하는 일련의 행위와 기술을 의미합니다.
웹은 개방된 인터넷 환경에서 동작하므로 항상 다양한 위협에 노출되어 있으며, 개발자는 이러한 위협을 인지하고 적절한 방어책을 마련해야 할 책임이 있습니다.
DEFENSE LAYERS
요청 경로마다 방어 책임을 나눕니다
HTTPS가 보호한 요청에도 공격 입력이나 권한 없는 작업이 들어올 수 있습니다. 서버는 외부 입력과 요청자의 허용 범위를 다시 확인합니다.
브라우저 · 출력과 조작
일반 텍스트는 안전하게 출력하고 HTML은 필요한 경우 정화합니다. CSP는 보완 통제이며, 브라우저의 폼 검증과 숨긴 버튼은 서버 권한 검사를 대신하지 않습니다.
전송·엣지 · 연결과 남용
HTTPS 인증서 검증·전송 보호, 요청 제한과 필터링을 담당합니다. 암호화된 연결이 입력 내용의 안전성을 보장하지는 않습니다.
서버 · 요청과 작업
형식·길이 등 입력을 검증하고 보호 자원의 인증·인가를 확인합니다. 쿠키 기반 상태 변경에는 별도의 CSRF 보호가 필요합니다.
데이터 접근 · 쿼리와 권한
SQL 문장과 값을 분리하고 DB 계정에 최소 권한을 부여합니다. 비밀번호는 전송 암호화와 별도로 안전한 해시로 저장합니다.
운영 · 탐지와 재검증
요청 식별자와 필요한 보안 로그로 의심 요청을 추적합니다. 비밀번호·세션·토큰 원문은 기록하지 않고 수정한 공격 시나리오를 다시 검증합니다.
화살표는 요청의 설명 순서이며 실제 미들웨어 배치 순서를 고정하지 않습니다. 서로 다른 계층의 통제가 각자의 실패를 다룹니다.
왜 웹 보안이 중요한가?
- 사용자 데이터 보호: 개인 정보(이름, 이메일, 주소, 전화번호), 금융 정보(신용카드 번호), 민감한 정보(건강 기록 등) 등 사용자의 중요한 데이터를 보호해야 합니다. 데이터 유출은 금전적 손실뿐만 아니라 기업 이미지에도 치명적인 영향을 미칠 수 있습니다.
- 서비스 신뢰성 유지: 서비스가 해킹당하거나 오작동하면 사용자는 해당 서비스를 신뢰하지 않게 됩니다. 이는 고객 이탈과 비즈니스 손실로 이어질 수 있습니다.
- 법적 책임: 개인 정보 보호법(GDPR, 국내 개인정보보호법 등)과 같은 법규를 준수하지 않을 경우, 법적인 처벌을 받을 수 있습니다.
- 서비스 가용성 보장: DDoS(Distributed Denial of Service) 공격 등으로 서비스가 마비되는 것을 방지하여, 사용자가 언제든 서비스를 이용할 수 있도록 해야 합니다.
주요 웹 공격 유형 및 방어 전략
ATTACK & DEFENSE
공격이 넘는 경계에서 방어를 선택합니다
전송·출력·SQL·요청 위조·인증·가용성은 서로 다른 실패입니다. 하나의 필터로 묶지 않고 각 경계에 맞는 통제를 적용합니다.
전송 · 도청과 변조
인증서를 검증하는 HTTPS/TLS로 전송을 보호합니다. HSTS는 브라우저가 HTTP 대신 HTTPS를 사용하도록 보강합니다.
출력 · XSS
일반 텍스트는 안전한 DOM API와 출력 문맥별 인코딩을 사용합니다. HTML 허용 시에는 검증된 정화 도구를 사용하며 CSP를 보완합니다.
SQL · 입력이 구문이 되는 경계
SQL과 값은 파라미터 API로 분리합니다. 동적 테이블·열 이름은 허용 목록으로 제한하고 DB 계정에는 필요한 권한만 줍니다.
상태 변경 요청 · CSRF
쿠키 인증 요청에 토큰 검증과 SameSite·출처 검사를 적용합니다. 토큰은 사용자의 의도를 완전히 증명하거나 인증·인가를 대신하지 않습니다.
인증 · 반복 로그인 시도
비밀번호를 솔트가 있는 적절한 해시로 저장하고 로그인 재시도를 제한합니다. 계정 잠금의 남용과 정상 사용자의 복구도 고려합니다.
가용성 · DDoS와 과부하
엣지의 필터링·요청 제한과 용량 분산을 함께 검토합니다. 로드 밸런서만으로 공격을 제거하거나 무한한 용량을 확보할 수는 없습니다.
입력 검증은 모든 경계의 공통 보완입니다. 출력 인코딩·SQL 파라미터화·서버 권한 검사처럼 실제 해석 지점의 방어를 대신하지 않습니다.
수많은 웹 공격 유형이 있지만, 개발자가 가장 흔하게 접하고 방어해야 할 몇 가지 중요한 공격들을 살펴보겠습니다.
XSS (Cross-Site Scripting)
- 개념: 공격자가 웹 사이트에 악성 스크립트(주로 JavaScript)를 삽입하여, 다른 사용자의 웹 브라우저에서 해당 스크립트가 실행되도록 하는 공격입니다. 이를 통해 세션 쿠키 탈취, 개인 정보 유출, 피싱 페이지로 리다이렉트 등의 피해를 입힐 수 있습니다.
-
공격 예시
- 게시판에
<script>alert(document.cookie);</script>같은 코드를 삽입. - URL 쿼리 파라미터에 악성 스크립트를 포함시켜 반사(Reflected) XSS 공격.
- 게시판에
-
방어 전략
- 입력 검증과 HTML 정화: 데이터의 허용 형식과 길이를 검증합니다. 일반 텍스트는 safe DOM API로 출력하고, HTML 입력을 허용해야 할 때만 검증된 sanitizer를 사용합니다. 태그나 위험 문자열의 단순 차단 목록만으로 XSS를 막을 수는 없습니다.
- 출력 문맥에 맞는 인코딩: HTML 본문·속성·URL 등 출력 위치에 맞는 인코딩을 적용합니다. HTML 엔티티 변환 하나로 JavaScript나 모든 URL 문맥을 보호할 수는 없습니다. React/Vue의 기본 텍스트 출력 보호를 유지하고, raw HTML 삽입에는 정화한 값만 사용합니다.
- CSP (Content Security Policy): 허용할 리소스 출처와 스크립트 실행 조건 등을 브라우저에 지시하는 보안 정책입니다. 정책의 내용에 따라 실행을 제한하므로, CSP 설정만으로 모든 XSS가 차단된다고 가정하지 않습니다. 출력 문맥에 맞는 처리와 함께 적용하는 추가 방어선입니다. HTTP 응답 헤더에
Content-Security-Policy를 설정합니다.
SQL Injection
- 개념: 공격자가 데이터베이스 쿼리를 조작하는 SQL 구문(데이터베이스 명령)을 웹 애플리케이션의 입력 필드(로그인 폼, 검색 폼 등)에 삽입하여, 데이터베이스를 비정상적으로 조작하거나 데이터를 탈취하는 공격입니다.
-
공격 예시
- 로그인 ID 입력 필드에
' OR 1=1 --를 입력하여 비밀번호 없이 로그인 성공. - 검색 필드에
'; DROP TABLE users; --를 입력하여 테이블 삭제 시도.
- 로그인 ID 입력 필드에
-
방어 전략
- 파라미터화된 쿼리: SQL 문장과 입력 값을 분리해 드라이버의 파라미터 API로 전달합니다. 테이블·열 이름 등 바인딩할 수 없는 식별자는 허용 목록으로 제한합니다. ORM을 써도 raw SQL 문자열 연결은 안전해지지 않으며, Mongoose는 SQL용 ORM이 아닙니다.
- 입력 값 검증: 사용자 입력 값의 타입, 길이, 허용 문자 등을 백엔드에서 엄격하게 검증합니다.
- 최소 권한 원칙: 데이터베이스 사용자에게 필요한 최소한의 권한만 부여합니다. (예:
SELECT권한만 필요한 계정은DROP권한을 주지 않음)
CSRF (Cross-Site Request Forgery)
- 개념: 사용자가 로그인한 상태에서 웹 사이트를 이용하고 있을 때, 공격자가 만든 악성 웹 페이지(또는 악성 스크립트)를 통해 사용자의 의지와 무관하게 특정 요청(예: 비밀번호 변경, 계좌 이체, 게시물 삭제)을 해당 웹 사이트에 보내도록 유도하는 공격입니다.
-
공격 예시
- 사용자가 로그인한 상태에서 악성 사이트 방문 시, 해당 사이트 내 숨겨진 폼이나 스크립트가 은행 사이트의 계좌 이체 URL로 요청을 보냄.
-
방어 전략
- CSRF 토큰 (CSRF Token): 서버가 예측하기 어려운 토큰을 발행하고, 폼 제출 때 함께 받아 유효성을 검증합니다. 토큰이 보호되고 서버 검증이 이루어지면 공격자가 다른 사이트에서 인증된 요청을 위조하기 어렵게 합니다. 토큰 유출이나 같은 출처에서 실행되는 XSS까지 이 방어 하나로 막는 것은 아닙니다.
- SameSite 쿠키 속성: 쿠키에
SameSite속성(Strict,Lax,None)을 설정하여, 교차 사이트 요청의 쿠키 전송을 제한합니다. 여기서 사이트는 보통 스킴과 등록 가능한 도메인으로 판단하므로 출처(origin)와 같지 않습니다.Strict는 교차 사이트 쿠키 전송을 더 제한합니다. 명시적인Lax는 다른 사이트에서 시작한 최상위 안전 메서드 탐색(예: 링크의 GET)을 허용하지만 일반적인 교차 사이트 POST는 허용하지 않습니다.None에는Secure가 필요합니다. 상태 변경을 GET으로 처리하지 않고 CSRF 토큰 등과 함께 검토합니다. - Referer 검증: HTTP
Referer헤더를 검사하여 요청이 신뢰할 수 있는 도메인에서 왔는지 확인합니다. (완벽하지는 않음)
Brute Force Attack (무작위 대입 공격)
- 개념: 공격자가 비밀번호, 핀 번호 또는 암호화 키를 찾아내기 위해 가능한 모든 조합을 시도하는 공격입니다. 특히 로그인 페이지에서 자주 발생합니다.
-
방어 전략
- 계정 잠금 (Account Lockout): 일정 횟수 이상 로그인 실패 시 계정을 일시적으로 잠급니다.
- 재시도 제한 (Rate Limiting): 특정 IP 주소에서 일정 시간 동안 허용된 요청 횟수를 제한합니다. (예: 1분당 로그인 시도 5회 제한)
- 캡차 (CAPTCHA): 자동화된 프로그램이 아닌 실제 사람임을 인증하는 절차(예: 그림 맞추기, 왜곡된 문자 입력)를 추가합니다.
- 비밀번호 정책: 충분한 길이와 유출·흔한 비밀번호 차단 목록을 적용하고 비밀번호 관리자 사용을 허용합니다. 임의의 문자 조합이나 정기 변경을 강제하지 않고, 유출 근거가 있을 때 변경을 요구합니다.
DDoS (Distributed Denial of Service)
- 개념: 여러 대의 좀비 PC(봇넷)를 사용하여 특정 서버에 대량의 트래픽을 동시에 발생시켜, 서버의 자원을 고갈시키고 정상적인 서비스가 불가능하게 만드는 공격입니다.
-
방어 전략
- CDN (Content Delivery Network): Cloudflare와 같은 CDN 서비스는 DDoS 공격을 분산시키고 필터링하는 데 도움을 줍니다.
- 방화벽 (Firewall): 비정상적인 트래픽을 탐지하고 차단합니다.
- 로드 밸런서 (Load Balancer): 트래픽을 여러 서버에 분산시켜 한 서버에 부하가 집중되는 것을 막습니다.
- 클라우드 서비스 활용: AWS, Azure, GCP 등 클라우드 서비스는 대규모 인프라를 통해 DDoS 공격에 대한 방어 능력이 뛰어납니다.
중요 보안 고려 사항
요청이 브라우저에서 서버와 데이터베이스까지 이동하는 동안 각 구간마다 담당해야 할 방어선이 다릅니다.
전송, 요청 위조, 입력 처리, 데이터 접근을 나누어 보면 빠진 조치를 더 쉽게 찾을 수 있습니다.
- HTTPS 사용: 모든 통신에 HTTPS(Hypertext Transfer Protocol Secure)를 사용해야 합니다. HTTP는 평문으로 데이터를 전송하여 중간에서 가로채기(Man-in-the-Middle) 공격에 취약합니다. HTTPS는 SSL/TLS 암호화를 통해 통신 내용을 보호하고, 서버의 신원을 인증합니다. (무료 인증서: Let's Encrypt)
- 비밀번호 해시 저장: 사용자 비밀번호는 평문으로 저장하지 않습니다. 복호화할 암호문 대신 솔트와 계산 비용을 갖춘 비밀번호 전용 해시로 저장하고 입력값을 같은 검증 절차로 비교합니다. 신규 시스템은 Argon2id를 우선 검토하고 사용할 수 없으면 scrypt 등 적절한 대안을 선택합니다. bcrypt는 기존 시스템의 호환 요구를 고려합니다.
- 입력 값 검증 (Input Validation): 모든 사용자 입력 값은 클라이언트와 서버 양쪽에서 엄격하게 검증해야 합니다. 클라이언트 측 유효성 검사는 사용자 경험을 위한 것이며, 서버 측 유효성 검사는 보안을 위한 필수적인 조치입니다.
- 오류 메시지 최소화: 사용자에게 불필요한 자세한 오류 메시지(예: 데이터베이스 에러 메시지)를 노출하지 않도록 합니다. 이는 공격자에게 시스템 정보를 제공할 수 있습니다.
- 보안 헤더 설정: HTTP 응답 헤더에
X-Content-Type-Options,X-Frame-Options,Strict-Transport-Security등 보안 관련 헤더를 설정하여 다양한 공격을 방어합니다. - 정기적인 보안 업데이트: 사용하는 프레임워크, 라이브러리, 운영체제 등을 항상 최신 버전으로 유지하여 알려진 취약점을 패치합니다.
작은 예제: 표시 이름을 검증하고 글자로 보여주기
입력 검증과 안전한 출력이 어디에서 필요한지 표시 이름 하나로 살펴보겠습니다. 이름을 보내면 서버가 문자열인지, 앞뒤 공백을 뺀 길이가 허용 범위인지 확인합니다. 브라우저는 돌려받은 이름을 HTML이 아닌 글자로 표시합니다. 이름을 저장하거나 회원을 만드는 기능은 없습니다.
폼 요소의 required·maxlength와 Express의 JSON 요청·응답을 연결하는 예제입니다. 아래 두 파일은 별도 폴더에서 사용할 전체 코드입니다. 앞 절의 회원가입 폼이나 사용자 CRUD API를 수정할 필요는 없습니다.
두 파일 준비하기
Node.js와 npm이 준비된 터미널에서 새 폴더를 만듭니다. 이 예제는 Express 5를 사용하며, Express 5의 최소 조건은 Node.js 18 이상입니다. 설치 환경이 아직 없다면 Node.js와 Express 시작하기를 먼저 읽습니다.
mkdir security-preview
cd security-preview
npm init -y
npm install express@5
mkdir public편집기에서 security-preview/public/index.html을 만들고 다음 내용을 작성합니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>표시 이름 미리보기</title>
</head>
<body>
<h1>표시 이름 미리보기</h1>
<form id="previewForm">
<label for="displayName">표시 이름</label>
<input id="displayName" name="displayName" type="text" required maxlength="40">
<button id="previewButton" type="submit">미리보기</button>
</form>
<p id="result" role="status" aria-live="polite"></p>
<script>
const form = document.getElementById('previewForm');
const input = document.getElementById('displayName');
const button = document.getElementById('previewButton');
const result = document.getElementById('result');
form.addEventListener('submit', async (event) => {
event.preventDefault();
button.disabled = true;
result.textContent = '';
try {
const response = await fetch('/api/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: input.value }),
});
const data = await response.json();
if (!response.ok) {
result.textContent = data && typeof data.message === 'string'
? data.message
: '입력을 확인해 주세요.';
return;
}
if (data === null || typeof data !== 'object'
|| Array.isArray(data) || typeof data.name !== 'string') {
result.textContent = '응답 형식이 다릅니다. Network의 Response를 확인하세요.';
return;
}
result.textContent = data.name;
} catch {
result.textContent = '요청을 완료하지 못했습니다. Network와 서버 터미널을 확인하세요.';
} finally {
button.disabled = false;
}
});
</script>
</body>
</html>같은 폴더의 security-preview/security-demo.cjs에는 서버 코드를 작성합니다. .cjs 확장자는 이 파일에서 CommonJS의 require를 사용한다는 뜻입니다.
const express = require('express');
const path = require('node:path');
const app = express();
const port = Number(process.env.PORT || 3000);
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
app.post('/api/preview', (req, res) => {
const rawName = req.body?.name;
if (typeof rawName !== 'string') {
return res.status(400).json({ message: 'name은 문자열이어야 합니다.' });
}
const name = rawName.trim();
if (name.length < 1 || name.length > 40) {
return res.status(400).json({
message: '앞뒤 공백을 뺀 표시 이름은 길이가 1~40이어야 합니다.',
});
}
return res.status(200).json({ name });
});
app.listen(port, '127.0.0.1', () => {
console.log('미리보기 주소: http://127.0.0.1:' + port);
});express.json()으로 읽은 값도 사용자가 보낸 입력입니다. 그래서 trim()을 호출하기 전에 문자열인지 확인합니다. 문자열이면 앞뒤 공백을 제거한 뒤 길이를 검사하고, 조건을 만족할 때만 HTTP 200과 { name }을 돌려줍니다. 여기의 length와 HTML의 maxlength는 UTF-16 코드 단위 기준입니다. 한글 음절 하나는 보통 1이지만 일부 이모지는 2 이상이므로, 화면에서 보이는 글자 수와 항상 같지는 않습니다.
서버를 열고 입력 비교하기
security-preview 폴더에서 서버를 시작합니다.
node security-demo.cjs터미널에 표시된 http://127.0.0.1:3000을 브라우저로 엽니다. HTML 파일을 직접 열지 마세요. 서버가 public 폴더의 화면과 /api/preview를 같은 주소·포트로 제공하므로, 화면의 상대 경로 요청도 같은 서버로 갑니다.
EADDRINUSE가 나오면 3000번 포트가 이미 사용 중입니다. 기존 서버는 그대로 두고 다른 포트를 지정합니다. PowerShell에서는 다음처럼 실행합니다.
$env:PORT = '3001'
node security-demo.cjsmacOS·Linux 셸에서는 PORT=3001 node security-demo.cjs로 실행할 수 있습니다. 포트를 바꿨다면 브라우저에서도 http://127.0.0.1:3001을 엽니다.
처음에는 민수를 입력하고 미리보기를 누릅니다. 화면에 이름이 나오면 개발자 도구의 Network에서 POST /api/preview의 상태 코드와 Response를 함께 봅니다. 그다음 입력을 바꿔 아래 결과를 비교합니다.
| 입력과 보내는 방법 | 확인할 결과 |
|---|---|
폼에 민수 입력 | HTTP 200, JSON {"name":"민수"}, 화면에 민수 |
폼에 민수 입력 | HTTP 200, 앞뒤 공백을 뺀 민수가 응답과 화면에 표시됨 |
| 폼을 비운 채 제출 | 브라우저의 필수 입력 안내. 이 제출로는 API 요청이 나가지 않음 |
| 폼에 공백 세 개만 입력 | 브라우저 검사는 통과하지만 서버가 HTTP 400으로 거절. 화면에 길이 안내 |
name을 숫자 42로 직접 요청 | HTTP 400, 콘솔에 name은 문자열이어야 합니다.를 담은 JSON |
name을 ASCII 문자 41개로 직접 요청 | HTTP 400, 콘솔에 길이 안내를 담은 JSON |
폼에 <b>안녕</b> 입력 | HTTP 200. 굵은 글씨로 바뀌지 않고 <b>안녕</b>가 그대로 표시됨 |
일반적인 폼 입력에서는 maxlength가 길이를 제한하고 input.value가 문자열을 돌려줍니다. 서버의 숫자·길이 거절을 보려면 이 페이지의 개발자 도구 Console에서 폼을 거치지 않고 요청합니다.
async function requestPreview(name) {
const response = await fetch('/api/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
});
console.log(response.status, await response.json());
}
requestPreview(42);첫 요청의 상태와 JSON을 읽은 뒤 requestPreview('a'.repeat(41));도 입력합니다. 둘 다 HTTP 400이어야 합니다. 이 직접 요청은 화면의 제출 처리 함수를 거치지 않으므로 결과는 콘솔에서 확인합니다. 표의 요청은 문법이 올바른 JSON을 전제로 하며, JSON 안의 name 값 규칙을 비교합니다.
결과가 다르면 먼저 Network의 요청 주소·상태 코드·응답 JSON을 확인합니다. 표의 HTTP 400은 요청이 도착했지만 이름 규칙에 맞지 않은 경우입니다. 연결 실패나 JSON 읽기 실패는 화면의 일반 오류 안내로 이어지므로, 주소와 서버 터미널도 봅니다. HTTP 성공이어도 name 문자열이 없는 응답은 화면에서 형식 오류로 처리합니다.
이 예제가 보호하는 경계
브라우저의 required·maxlength는 입력을 돕지만 직접 요청을 막지는 못합니다. 서버는 문자열 타입과 앞뒤 공백을 뺀 길이를 직접 확인해야 합니다. Express의 요청 본문 설명도 외부 입력의 모양과 값을 사용하기 전에 검증하도록 안내합니다.
길이를 통과한 <b>안녕</b>를 서버가 받아도, 브라우저는 result.textContent에 넣어 글자로 표시합니다. 입력에서 꺾쇠를 지웠기 때문이 아니라 HTML로 해석하는 위치에 넣지 않았기 때문입니다. MDN의 textContent 설명과 OWASP의 안전한 출력 위치 설명에서 이 구별을 더 볼 수 있습니다.
이 방식은 일반 요소의 텍스트 출력에 적용합니다. 사용자가 작성한 HTML을 허용하거나 값을 URL·HTML 속성·JavaScript 코드에 넣는 경우에는 해당 문맥에 맞는 별도 처리가 필요합니다. 위 코드는 로그인·DB 저장·CSRF 방어·CSP·HTTPS를 구현하지 않았으며, 서비스 전체의 보안을 확인하는 예제가 아닙니다. 앞의 방어선 그림에서 서버의 입력 검사와 브라우저의 텍스트 출력만 연결한 작은 사례입니다.
다른 이름을 제출하면 결과 영역이 새 응답으로 바뀝니다. 결과 영역을 비우고 다시 시작하려면 페이지를 새로고침합니다. 이 코드는 서버나 브라우저 저장소에 이름을 보관하지 않으므로 이 예제에서 삭제할 데이터는 없습니다. 마칠 때는 이 서버를 띄운 터미널에서 Ctrl+C를 누릅니다. PowerShell에서 임시 포트를 지정했다면 같은 터미널에서 Remove-Item Env:PORT로 해제할 수 있습니다.
예시
이름 길이는 서버가 받은 원문에서 바로 검사하지 않고 trim()한 뒤 검사합니다. 앞에 공백 하나와 a 40개를 보내면 원문 길이는 41이지만 저장하지 않는 미리보기 값은 길이 40이어서 200입니다. 공백만 40개라면 길이가 0이 되어 400입니다. 브라우저 폼의 maxlength가 있어도 직접 보낸 JSON 요청에는 서버의 판단이 필요합니다.
서버에 유효한 JSON으로 직접 요청한다고 가정합니다. ① a 39개 뒤에 이모지 😀 하나를 넣은 이름, ② a 38개 뒤에 같은 이모지 하나를 넣은 이름, ③ 숫자 42를 name 값으로 보낸 요청의 상태와 이유를 먼저 써 보세요. ④ 문자열 <b>안녕</b>가 200으로 응답되어도 화면의 굵은 글씨가 되어서는 안 되는 이유를 설명하세요.
판단 과정 확인
① 이 이모지는 UTF-16 코드 단위 두 개이므로 39 + 2 = 41로 길이 검사에서 400입니다. ② 38 + 2 = 40이어서 200입니다. 눈에 보이는 글자 수를 세는 조건과 이 예제의 코드 단위 길이를 구별합니다. ③ 문자열이 아니므로 trim()을 호출하기 전에 첫 타입 검사에서 400입니다. ④ textContent는 일반 요소의 텍스트를 채우므로 태그 구문이 문서 요소로 해석되지 않습니다. 이름 타입·길이 검사와 출력 해석 지점의 보호는 서로 다른 조건입니다. 이 응답 성공은 로그인·저장·CSRF 방어가 구현됐다는 뜻이 아닙니다.
OWASP의 출력 문맥과 안전한 sink 설명, SameSite의 보완적 역할, 비밀번호 해시 선택 원칙을 이 예제의 보호 범위와 함께 확인하세요.
보안 점검 흐름 (Step-by-Step)
아래는 서비스 전체에서 확인할 점검 흐름입니다. 표시 이름 미리보기에서 다룬 입력·출력 외의 항목은 각 기능과 운영 환경에 맞춰 별도로 준비해야 합니다.
공격면 정리: 인증, 입력 폼, 파일 업로드, 관리자 API 등 외부 입력 지점을 먼저 목록화합니다.
기본 방어선 확인: HTTPS, 보안 헤더, 비밀번호 해시, CSRF 보호, 레이트 리미트 적용 여부를 점검합니다.
침투 시나리오 테스트: XSS/SQL Injection/권한 우회 시나리오를 재현하여 실제 방어 동작을 검증합니다.
로그/탐지 체계 점검: 의심 요청이 로그에 남는지, 알림 체계가 있는지 확인합니다.
조치 및 회귀 테스트: 수정 후 동일 시나리오를 다시 실행해 재발을 차단합니다.
보안 적용 검증 체크리스트
개념 점검- 인증(Authentication)과 인가(Authorization)의 경계를 설명할 수 있는가?
- XSS/SQL Injection/CSRF의 차이와 대응 전략을 구분할 수 있는가?
- 민감 API에 대해 인증, 인가, 입력 검증이 모두 적용되었는가?
- 비정상 요청(과도한 로그인 시도, 의심 파라미터)을 로그/알림으로 추적할 수 있는가?
- 보안 설정 변경 후 핵심 사용자 시나리오(로그인, 결제, 권한 기능)가 정상 동작하는가?
보안 점검은 공격 이름별 암기보다 요청이 지나가는 구간별 방어선을 빠짐없이 세우는 방식으로 보는 편이 실전적입니다.
마지막으로 보안 조치를 요청 이동 경로별로 배치해 빠진 방어선이 없는지 확인합니다.
웹 보안 기초 정리
핵심은 공격 유형을 암기하는 것이 아니라 입력·인증·전송 구간별 방어 책임을 나누는 것입니다.
이번 장에서는 웹 애플리케이션을 개발하고 운영하는 데 있어 가장 중요한 요소 중 하나인 웹 보안의 기초 개념과 주요 위협 및 방어 전략을 학습했습니다.
이번 절에서는 XSS, SQL Injection, CSRF, Brute Force, DDoS 같은 공격 유형과 입력 검증, Prepared Statement, CSRF 토큰, 재시도 제한, HTTPS, 비밀번호 해시 저장 같은 방어 기준을 정리했습니다.
웹 보안은 초기 설계부터 운영까지 이어지는 점검 항목입니다.
입력 검증, HTTPS, 비밀번호 저장 방식, 의존성 업데이트 같은 기준을 지속적으로 확인해야 합니다.