SQL 인젝션, XSS, CSRF는 어떻게 막을까?
관리자 페이지 하나를 급하게 만들고 배포했는데 며칠 뒤 이상한 요청 로그가 쌓이기 시작한다. 게시판에는 알 수 없는 스크립트가 등록되고, 로그인된 관리자 계정으로 의심스러운 요청이 발생한다. 실제 보안 사고 상당수는 거대한 해킹 기술보다 기본적인 입력값 처리 실수에서 시작된다.
웹 보안 입문 단계에서 가장 먼저 이해해야 하는 공격은 SQL 인젝션, XSS, CSRF다. 오래된 취약점처럼 보이지만 지금도 반복적으로 발견된다. 이유는 단순하다. 대부분의 웹 서비스는 결국 사용자 입력을 처리하고, 로그인 상태를 유지하며, 브라우저를 통해 동작하기 때문이다.
중요한 것은 공격 이름을 외우는 것이 아니다. 데이터가 어디로 들어오고, 어떤 방식으로 실행되며, 브라우저와 서버가 어떤 신뢰 관계를 형성하는지 이해해야 실제 보안 구조가 보인다. 웹 보안은 공격 종류 암기보다 데이터 흐름의 신뢰 경계를 이해하는 과정에 더 가깝다.
웹 서비스는 왜 반복해서 같은 공격에 당할까
대부분의 웹 공격은 입력값 검증 실패에서 시작된다. 사용자가 입력한 값을 안전하다고 가정하는 순간 공격 가능성이 열린다.
웹 서비스는 로그인, 검색, 댓글, 게시판, 파일 업로드처럼 끊임없이 외부 입력을 받는다. 문제는 개발자가 예상한 입력만 들어오지 않는다는 점이다. 공격자는 SQL 구문을 넣거나, 자바스크립트 코드를 삽입하거나, 브라우저가 자동 전송하는 인증 정보를 악용한다.
특히 초보 개발 단계에서는 기능 구현 속도가 우선이 되기 쉽다. “나중에 보안 처리하면 된다”는 접근이 반복되면서 취약점이 남는다. 실제 운영 환경에서는 이런 작은 누락 하나가 계정 탈취나 데이터 유출로 이어질 수 있다.
많은 개발자가 프레임워크를 사용하면 자동으로 안전해진다고 생각한다. 하지만 프레임워크는 기본 방어 기능을 제공할 뿐이다. 문자열 연결 방식으로 직접 쿼리를 만들거나, 사용자 입력 HTML을 그대로 출력하면 여전히 취약점이 발생한다.
세 공격 모두 결국 서버와 브라우저가 사용자의 입력을 어디까지 신뢰하느냐에서 시작된다. 실무에서는 “신뢰하지 않는다”가 기본 원칙이다. 사용자의 입력, 브라우저 요청, 쿠키 값, API 파라미터까지 모두 검증 대상이라고 보는 편이 안전하다.
STEP 1. SQL 인젝션은 데이터베이스 구조를 노린다
SQL 인젝션은 사용자 입력을 이용해 데이터베이스 쿼리를 조작하는 공격이다. 가장 오래된 웹 공격 중 하나지만 여전히 매우 위험하다.
예를 들어 로그인 기능에서 문자열 연결 방식으로 쿼리를 만들면 문제가 발생할 수 있다.
SELECT * FROM users
WHERE id = '$id' AND password = '$password'
공격자가 입력값에 SQL 구문을 삽입하면 조건 자체가 변조될 수 있다. 과거에는 ' OR 1=1 -- 같은 단순한 구문만으로 인증 우회가 발생하는 사례도 많았다.
위험한 이유는 데이터베이스 자체를 직접 건드릴 수 있기 때문이다. 단순 로그인 우회뿐 아니라 사용자 정보 조회, 테이블 삭제, 관리자 계정 생성 같은 문제로 이어질 수 있다.
현재 실무에서 기본 방어 방식은 Prepared Statement다. 쿼리 구조와 사용자 입력 데이터를 분리해 SQL 구문 자체가 변경되지 않도록 만든다.
| 위험한 방식 | 안전한 방식 |
|---|---|
| 문자열 연결 쿼리 | Prepared Statement |
| Raw Query 직접 조합 | Parameter Binding |
| 사용자 입력 직접 삽입 | ORM 바인딩 사용 |
ORM을 사용한다고 해서 무조건 안전해지는 것은 아니다. 대부분의 ORM은 안전한 바인딩 구조를 제공하지만, Raw Query를 직접 작성하거나 문자열 조합을 사용하면 SQL 인젝션 가능성이 다시 생긴다.
실제 보안 점검에서도 관리자 검색 기능이나 내부 운영 도구처럼 “외부 노출이 적다”고 생각한 영역에서 SQL 인젝션이 자주 발견된다. 운영 편의를 위해 빠르게 만든 기능이 오히려 가장 위험해지는 경우가 많다.
STEP 2. XSS는 사용자 브라우저를 공격한다
XSS는 Cross Site Scripting의 약자다. 공격자가 웹 페이지에 스크립트를 삽입해 다른 사용자의 브라우저에서 실행되도록 만드는 공격이다.
대표적으로 게시판이나 댓글 기능에서 많이 발생한다. 사용자가 입력한 HTML이나 스크립트를 검증 없이 그대로 출력하면 브라우저가 실제 코드로 실행할 수 있다.
Stored XSS는 악성 스크립트가 서버에 저장되는 형태다. 게시글이나 댓글에 삽입된 스크립트가 이후 접속하는 모든 사용자 브라우저에서 실행된다. 영향 범위가 매우 크다.
Reflected XSS는 요청 파라미터에 포함된 스크립트가 즉시 반사되어 실행되는 형태다. 주로 검색 기능이나 URL 파라미터에서 자주 발생한다.
XSS가 위험한 이유는 브라우저 세션 자체를 노릴 수 있기 때문이다. 공격자는 쿠키 탈취, 관리자 세션 가로채기, 피싱 페이지 삽입, 악성 API 요청 실행 같은 공격을 수행할 수 있다.
특히 JWT를 localStorage에 저장하는 구조에서는 XSS 위험이 더 커질 수 있다. 스크립트가 localStorage에 접근해 토큰을 탈취할 가능성이 있기 때문이다. 반대로 HttpOnly Cookie 기반 구조는 자바스크립트 접근을 제한할 수 있어 상대적으로 안전하다.
많은 프론트엔드 프레임워크는 자동 이스케이프 기능을 제공한다. React나 Vue 역시 기본 출력에서는 비교적 안전한 편이다. 하지만 React의 dangerouslySetInnerHTML 같은 기능을 사용하거나 HTML 렌더링을 직접 처리하면 XSS 가능성이 다시 생긴다.
기본 방어 원칙은 출력 시 이스케이프다. 사용자 입력 데이터를 HTML로 그대로 출력하지 않고 특수 문자를 변환해 브라우저가 코드로 인식하지 못하게 만들어야 한다.
최근에는 CSP(Content Security Policy)를 함께 적용해 허용되지 않은 스크립트 실행 자체를 제한하는 방식도 많이 사용된다.
STEP 3. CSRF는 로그인 상태를 악용한다
CSRF는 사용자가 이미 로그인되어 있다는 점을 악용하는 공격이다. 사용자가 직접 요청하지 않았는데도 브라우저가 자동으로 인증 정보를 함께 보내는 구조를 이용한다.
예를 들어 사용자가 은행 사이트에 로그인한 상태라고 가정해보자. 동시에 악성 사이트에 접속하면 브라우저는 자동으로 쿠키를 포함한 요청을 전송할 수 있다. 서버가 이를 정상 요청으로 오해하면 송금이나 정보 변경 같은 작업이 실행될 가능성이 생긴다.
CSRF는 “사용자 인증이 이미 되어 있다”는 점을 역으로 이용한다는 점에서 위험하다. 공격자는 비밀번호를 몰라도 사용자의 로그인 상태만 유지되면 공격을 시도할 수 있다.
현재 가장 기본적인 방어 방식은 CSRF Token 사용이다. 서버가 예측 불가능한 토큰을 생성하고 요청마다 함께 검증하는 방식이다. 공격자는 정상 토큰 값을 알 수 없기 때문에 요청 위조가 어려워진다.
최근 브라우저에서는 SameSite Cookie 정책도 중요한 방어 수단이 되고 있다. SameSite 설정을 사용하면 외부 사이트에서 자동으로 쿠키가 전송되는 상황을 제한할 수 있다.
- CSRF Token은 요청 위조 여부를 검증한다.
- SameSite Cookie는 외부 사이트 쿠키 전송을 제한한다.
- Referer와 Origin 검증은 요청 출처를 확인한다.
다만 SameSite만으로 모든 CSRF가 해결되는 것은 아니다. 실무에서는 여러 방어 기법을 함께 사용하는 경우가 많다.
JWT 기반 인증이라고 해서 CSRF 문제가 완전히 사라지는 것도 아니다. JWT를 Cookie에 저장하는 구조라면 브라우저가 자동으로 쿠키를 전송할 수 있기 때문에 여전히 CSRF 방어가 필요하다.
STEP 4. 입력값 검증과 출력 이스케이프를 분리해야 한다
입력값 검증과 출력 이스케이프는 자주 함께 언급되지만 실제 역할은 완전히 다르다. 둘을 혼동하면 보안 처리가 누락되기 쉽다.
입력값 검증은 “허용 가능한 데이터만 받는 과정”이다. 숫자만 입력 가능한 필드라면 숫자 외 문자를 차단하고, 이메일 형식이라면 정규식을 통해 검증하는 식이다.
하지만 입력 검증만으로는 충분하지 않다. 정상 입력처럼 보이는 값도 브라우저 출력 과정에서 위험해질 수 있기 때문이다.
예를 들어 게시글 내용에 <script> 태그가 포함되어 있다면 서버 저장 자체는 가능할 수 있다. 문제는 이를 HTML로 출력할 때 발생한다. 브라우저가 스크립트로 해석하면서 XSS가 실행될 수 있다.
그래서 실무에서는 입력 검증과 출력 이스케이프를 별도로 관리한다. 저장 가능한 데이터 범위를 제한하면서도, 최종 출력 시에는 HTML 특수 문자를 안전하게 변환한다.
프론트엔드 프레임워크가 자동 이스케이프 기능을 제공하더라도 예외 상황은 존재한다. HTML 렌더링 기능이나 Markdown 미리보기 기능처럼 직접 렌더링을 처리하는 영역에서는 별도 보안 처리가 필요하다.
STEP 5. 실무에서는 단일 방어보다 다층 보안을 사용한다
실제 서비스에서는 하나의 보안 기술만으로 공격을 막지 않는다. 여러 계층을 동시에 보호하는 다층 보안 구조를 사용한다.
WAF(Web Application Firewall)는 비정상 요청 패턴을 탐지하고 차단하는 역할을 한다. SQL 인젝션이나 비정상 트래픽을 1차적으로 필터링하는 데 많이 사용된다.
CSP(Content Security Policy)는 브라우저에서 실행 가능한 스크립트 범위를 제한한다. XSS 피해 범위를 줄이는 데 효과적이다.
Rate Limit은 짧은 시간 안에 과도한 요청이 들어오는 상황을 제한한다. 로그인 무차별 대입 공격이나 API 남용 방지에 자주 사용된다.
실무에서는 인증 구조와 세션 관리 정책까지 함께 고려한다. JWT 만료 시간을 짧게 유지하고, Refresh Token을 분리하며, HttpOnly Cookie를 사용하는 이유도 모두 같은 흐름이다.
최근에는 관리자 페이지 접근 제한, MFA 인증, IP 기반 접근 제어 같은 정책까지 함께 적용하는 경우가 많다. 실제 보안은 특정 기술 하나로 해결되지 않는다. 운영 구조 전체를 함께 관리해야 효과가 나온다.

보안 입문자가 가장 자주 하는 위험한 착각
ORM을 사용하면 SQL 인젝션이 완전히 사라진다고 생각하는 경우가 많다. 하지만 Raw Query를 직접 작성하거나 문자열 연결을 사용하면 여전히 취약점이 발생할 수 있다.
HTTPS만 적용하면 모든 공격이 막힌다고 오해하는 경우도 많다. TLS는 통신 보호 역할일 뿐 XSS나 CSRF 자체를 해결하지는 않는다.
관리자 페이지 URL을 숨기면 안전하다고 생각하는 것도 위험하다. 실제 공격자는 크롤링, 브루트포스, 로그 분석 등 다양한 방식으로 관리자 경로를 탐색한다.
또 하나 자주 발생하는 문제는 “내 서비스는 작은 규모라 공격받지 않는다”는 생각이다. 실제 자동화 공격은 규모와 상관없이 인터넷 전체를 대상으로 반복적으로 수행된다.
보안은 특별한 기능이 아니라 기본 설계에 가깝다. 기능 개발 이후 뒤늦게 추가하는 것이 아니라 데이터 흐름과 인증 구조를 처음부터 안전하게 설계하는 접근이 중요하다.
SSL/TLS와 JWT 구조까지 함께 이해하면 웹 보안 흐름이 더 선명해진다
SQL 인젝션, XSS, CSRF 같은 공격을 제대로 이해하려면 결국 인증과 통신 구조까지 함께 볼 필요가 있다. 특히 HTTPS 기반 TLS 암호화, JWT 인증 구조, OAuth 로그인 흐름은 실제 웹 보안의 기본 축에 가깝다.
이전 글인 “암호화와 인증 기초”를 함께 보면 왜 쿠키 보호, 토큰 저장 방식, 인증 요청 구조가 중요한지 훨씬 자연스럽게 연결해서 이해할 수 있다.
