◆ TURNING · DB

SQL 인젝션 직접 시연

#{} vs ${} — 한 글자 차이의 보안 차이

학습 목표

  • SQL 인젝션이 무엇인지 안다
  • 위험한 코드를 직접 만들고 공격해본다
  • MyBatis 의 #{} 와 ${} 의 차이를 안다
  • 앞으로 모든 SQL 에서 #{} 만 쓰는 습관을 만든다

⚠️ 안전한 코드 같지만

자연스러워 보이는 로그인 코드

<select id="login" resultType="com.smhrd.domain.Member">
    SELECT * FROM mymember
    WHERE id  = '${id}'
      AND pwd = '${pwd}'
</select>

한 줄 짜리 SQL 처럼 보이고 — 실제로 동작합니다. 그런데 비밀번호 없이도 누구든 admin 으로 로그인할 수 있는 위험한 코드.

🛠️ SQL 인젝션이란

악의적 입력으로 SQL 문 자체를 조작하는 공격

사용자 입력이 SQL 문에 그대로 끼워 넣어질 때 — 입력값에 SQL 코드를 함께 넣어 원래 의도와 다른 SQL 을 만드는 것.

정상 입력: id = "hong" SQL: SELECT * FROM mymember WHERE id = 'hong' 악의적 입력: id = "admin' OR '1'='1" SQL: SELECT * FROM mymember WHERE id = 'admin' OR '1'='1' ↑ WHERE 절이 항상 참 → 모든 회원 조회됨

위험 코드 실험 — 환경 구성


INSERT INTO mymember(id, pwd) VALUES
    ('admin', 'admin1234'),
    ('hong',  'hongpw');

<!-- 의도적으로 위험한 SELECT -->
<select id="loginUnsafe" resultType="com.smhrd.domain.Member">
    SELECT * FROM mymember
    WHERE id  = '${id}'
      AND pwd = '${pwd}'
</select>

public Member loginUnsafe(String id, String pwd) {
    return mapper.loginUnsafe(id, pwd);
}

공격 ① — 비밀번호 우회

사용자 입력: id = admin' -- pwd = (아무거나) 완성된 SQL: SELECT * FROM mymember WHERE id = 'admin' --' AND pwd = '...' ↑ -- 뒤는 주석 처리 비밀번호 검사 자체가 사라짐 결과: admin 계정에 비밀번호 없이 로그인 성공

공격 ② — 모든 계정 조회

사용자 입력: id = anything' OR '1'='1 pwd = anything' OR '1'='1 완성된 SQL: SELECT * FROM mymember WHERE id = 'anything' OR '1'='1' AND pwd = 'anything' OR '1'='1' '1'='1' 은 항상 true → WHERE 가 사실상 무력화 → 모든 회원이 조회됨 → 첫 행이 admin 이면 admin 으로 로그인됨

공격 ③ — 데이터 변조 / 삭제

사용자 입력: id = hong'; DROP TABLE mymember; -- 완성된 SQL: SELECT * FROM mymember WHERE id = 'hong'; DROP TABLE mymember; --'; → 두 SQL 이 연속 실행 → 회원 테이블 통째로 삭제! ※ MyBatis 가 한 번에 한 SQL 만 실행하면 막히기도 하지만, 설정·DB 따라 가능. 절대 의지 X.

왜 이런 일이 일어나나

${} 는 사용자 입력을 SQL 문자열에 그대로 끼워넣음:


"... WHERE id = '" + id + "'"

사용자가 따옴표를 입력하면 — SQL 문법 구조가 부서집니다. 입력값과 SQL 문이 섞여버림.

🛠️ 해법 — #{} 사용


<!-- 안전한 코드 -->
<select id="login" resultType="com.smhrd.domain.Member">
    SELECT * FROM mymember
    WHERE id  = #{id}
      AND pwd = #{pwd}
</select>

👉 따옴표 없음! #{} 자체가 PreparedStatement 의 ? 로 변환됨.

#{} 의 작동 — PreparedStatement

${} (위험) MyBatis: 문자열 직접 치환 → SQL: WHERE id = 'admin' OR '1'='1' → DB: 이 글자 그대로 SQL 로 해석 → 인젝션 가능 #{} (안전) MyBatis: PreparedStatement 의 ? 로 치환 → SQL: WHERE id = ? 파라미터: ['admin' OR '1'='1'] → DB: ? 자리에 「값」으로만 끼움 값 안의 ', ; 등이 SQL 문법으로 해석되지 않음 → 인젝션 불가

한 글자 차이의 보안 격차

${} — 위험

WHERE id = '${id}'

SQL 문자열 직접 치환

사용자 입력이 SQL 문법으로 해석

인젝션 가능

#{} — 안전

WHERE id = #{id}

PreparedStatement 자동 변환

사용자 입력이 「값」으로만 처리

인젝션 불가

그럼 ${} 는 언제 쓰나?

${} 가 필요한 경우는 매우 제한적:

  • 테이블명 / 컬럼명을 동적으로 — PreparedStatement 의 ? 가 값만 받음
  • ORDER BY 의 컬럼명ORDER BY ${sortColumn}
  • 동적 LIMIT / OFFSET — 일부 DB

이 경우에도 입력값을 화이트리스트로 검증한 후에만 ${} 사용:


List<String> allowed = List.of("num", "title", "writer");
if (!allowed.contains(sortColumn)) {
    throw new IllegalArgumentException();
}

실전 — 우리 코드의 모든 SQL 점검

기본 규칙
  1. 모든 파라미터는 #{}
  2. ${} 는 동적 컬럼·테이블명에만
  3. ${} 사용 시 화이트리스트 검증 필수
  4. 코드 리뷰에서 ${} 가 보이면 무조건 일시 정지

IDE 에서 ${} 를 검색하는 습관을 들이세요. 본 과정의 게시판 / 회원 SQL 에서 ${} 가 보이면 안 됩니다.

SQL 인젝션은 OWASP Top 10 의 단골

OWASP (Open Worldwide Application Security Project) 가 매년 발표하는 「가장 위험한 보안 취약점 Top 10」 에 SQL 인젝션은 20 년 넘게 상위권입니다.

현실적인 피해 사례:

  • 2017 년 Equifax — 약 1.5 억 명 정보 유출
  • 2014 년 한국 카드 3 사 — 약 1 억 건 정보 유출 (관련 부수 취약점)
  • 매년 수많은 중소 사이트 — 회원 정보 / 게시판 데이터 탈취

다른 보안 함정과의 관계

취약점본 과정 차시
SQL 인젝션이 차시
평문 비밀번호★ v3 안전 인증 (BCrypt)
인가 부재 (남의 글 수정)★ v6 안전 게시판
XSS (스크립트 삽입)JSP <c:out> / EL escape (개념 소개)
CSRF후속 과정 (Spring Security)

실험 — 직접 공격해보기

  1. 위험한 ${} 버전의 로그인 SQL 작성
  2. 로그인 폼에서 ID 에 admin' --, 비밀번호 아무거나
  3. 로그인 성공! (서버 콘솔 SQL 로그도 확인)
  4. SQL 을 #{} 로 변경 후 재시도
  5. "존재하지 않는 회원" — 정상 차단됨

👉 한 번 직접 깨뜨려보면 ${} 를 평생 안 쓰게 됩니다.

확장 — 다른 DB·다른 ORM 도 같음

도구안전한 방식
JDBC 직접PreparedStatement.setXxx(idx, value)
MyBatis#{}
JPA / HibernateJPQL 의 :파라미터
Python (psycopg2)cur.execute(sql, (params,))
Node.js (mysql2)conn.query(sql, [params])

👉 어느 언어를 써도 「SQL 과 데이터를 분리」 의 발상은 같음.

🔄 Before / After

전 차시 끝

Mapper XML 의 #{}${} 가 둘 다 보이는데 차이 모름.

이번 차시 끝

한 글자 차이가 보안의 전부임을 직접 봤다. 모든 SQL 에 #{} 만 쓰는 습관.

이번 차시의 데이터 흐름

사용자 입력
#{} → 값으로
PreparedStatement
DB
사용자 입력과 SQL 문이 「분리된 채로」 전달됨 — 인젝션의 길이 막힘

정리

오늘 들고 가는 것

  • SQL 인젝션 = 사용자 입력으로 SQL 문 구조 조작
  • ${} = 위험 (문자열 직접 치환)
  • #{} = 안전 (PreparedStatement)
  • 본 과정의 모든 SQL 은 #{} 사용
  • ${} 는 동적 컬럼명 같은 특수 경우 + 화이트리스트 검증

다음: @Transactional 기본 — 송금 비유로 트랜잭션 이해.