혹시 '데이터 베이스를' 아십니까 ? #6 – SQL Injection

전하윤·2025년 7월 8일
1

DB

목록 보기
6/7
post-thumbnail

목차


개요

데이터베이스 시리즈에서 이미 스프레드시트보다 DBMS를 사용해야 하는 여러 가지 이유를 다뤘습니다.
이번에는 한 걸음 더 나아가, DBMS를 사용할 때 반드시 주의해야 할 점,
특히 데이터베이스 보안에 대해 이야기하고자 합니다.

요즘 대부분의 웹 서버는 필수적으로 데이터베이스를 사용하고 있습니다.
하지만, 이러한 데이터베이스를 노리는 악의적인 공격자들도 많아졌고,
SQL Injection 같은 악의적인 쿼리 삽입,
권한 없는 사용자의 불필요한 데이터 접근,
개발자의 코드 미숙 등으로 인한 보안 취약점이 실무 현장에서도 여전히 자주 발생하고 있습니다.

이번 글에서는
데이터베이스 보안의 대표적인 위험과 그에 대한 방어 기법을
공격/방어 예시와 함께 구체적으로 정리해보았습니다.


SQL Injection이란?

출처:backhoe

SQL Injection이란 응용 프로그램 보안 상의 허점을 의도적으로 이용해, 악의적인 SQL문을 실행되게 함으로써 데이터베이스를 비정상적으로 조작하는 코드 인젝션 공격 방법입니다.

핵심 원리:

  • 사용자가 입력한 데이터가 그대로 쿼리에 들어가서
  • 의도하지 않은 SQL 명령을 실행할 수 있게 만듭니다.

SQL Injection 실전 공격 예시

⚠️ 본 실습은 해킹 연습용으로 허가된 사이트(해킹 실습 플랫폼)에서만 진행했습니다.
실제 운영 중인 웹사이트에서는 절대로 따라 하시면 안 됩니다!

🛑 비인가된 사이트에서의 해킹 시도는 불법이며, 법적 처벌을 받을 수 있습니다.
항상 윤리적 해커로서, 올바른 환경에서만 연습해 주세요.


자 제가 잠깐동안 해커가 되어서 여러분의 소중한 은행 계좌에 접근해 돈을 빼보겠습니다.

  1. 일단 로그인을 시도 해보지만, 아이디 비밀번호를 모르니 성공 할 리가 없죠.

  2. 이때 아이디에 '(따옴표)를 넣어서 한번 다시 시도 해봤습니다.

이렇게 했더니 로그인 처리에 관해 이상한 에러 메시지를 웹상에서 드러냈습니다.
딱 봐도 SQL 에러 코드인거 같은데 왜 이런 현상이 나왔을까요?


그 이유는 개발자가 관게형 데이터베이스를 활용해 아이디와 비밀번호를 받을때

SELECT * FROM accounts
WHERE id = '유저가 입력한 id' AND pw = '유저가 입력한 pw'

이렇게 받는데,

위에서 제가 로그인 할때 사용했던 aaa'를 넣게 된다면 어떻게 될까요?
내부에서 실행되는 쿼리

SELECT * FROM accounts
WHERE id = 'aaa' ' AND pw = '****'

이렇게 쿼리문이 실행되면서 Sysntax error를 터트리게 됩니다.

자 그럼 이 사이트는 SQL Injection이 가능한 사이트라고 판단이 됩니다.
(뒤에 이유 설명)


강제로그인 1번

다시 로그인 페이지로 넘어와서 이번에는 이렇게 입력해보겠습니다.

SQL에서 '--'는 뒤에 문장을 주석 처리 하겠다는 의미입니다.

tuser이란 사람이 있으면 그 즉시 로그인을 진행하게 만들고 비밀번호를 확인 하는 쿼리문은 모두 주석처리가 됩니다.

  • 내부에서 실행되는 쿼리
SELECT * FROM accounts
WHERE id = 'tuser -- ' AND pw = '****'

성공


강제로그인 2번

이번에는 다른 방법을 활용해 로그인을 해보겠습니다.

OR 1=1은 항상 참(True)입니다.

이렇게 되면 예를들어 이렇게 데이터베이스에 tuser이란 회원 정보가 들어있다고 가정하면 Select * 조건에 만족하는 모든 행을 가져올겁니다.

이후 대부분의 로그인 로직에서는 아래와 같이 회원을 반환하는 로직을 작성합니다.

if (rs.next()) {
   // 로그인 성공 처리
}

즉, username이 tuser가 아니더라도,
WHERE 조건을 만족하는 첫 번째(맨 위) 회원의 정보로 로그인 처리가 됩니다.

  • 만약 테이블이 이렇게 되어 있다면? (실제로도 admin 또는 test 계정을 가장 상단에서 생성하곤 합니다.)**

Admin 계정 로그인 성공

자 이제 어드민 계정으로 1억 달러 정도 제 계좌로 보내겠습니다.

이제 저는 부자가 되었습니다. 그 동안 벨로그를 읽어주셔서 감사합니다.


Union Select를 활용한 공격

이번에는 Union select 문법을 활용해서 다른 사이트에서 SQL Injection 공격을 진행 해보겠습니다.

해당사이트는 쿼리 파라미터에 작가의 id값을 넣어서 작가를 조회하는 방식으로 운영되는 방식인것을 미리 확인 했습니다.

그렇다면 작가 번호란에 Union Select문을 넣어서 SQL Injection 공격을 해보겠습니다.

짜잔 이렇게 해당 사이트의 어떤 유저의 아이디와 비밀번호를 훔치는데 성공 했습니다.

어떻게 된 일 일까요?


아마 해당 사이트는 작가 이름의 검색 쿼리를 이렇게 작성 했을겁니다.

SELECT * FROM 어쩌구
WHERE artis= ' '

이때 제가 url에 넣은 유니온 쿼리를 넣어서 다시 풀 쿼리로 보겠습니다.

SELECT * FROM 어쩌구
WHERE artist=-1 UNION
SELECT 1,uname,pass FROM users
WHERE uname =  'test'

해당 쿼리를 해석해보면

  • author_no = -1 조건이므로 원래 테이블에서는 결과가 없음(혹은 무의미한 결과)

  • UNION 키워드는 두 SELECT의 결과를 합쳐서 반환
    SELECT 1, uname, pass FROM users WHERE uname = 'test'

  • 1은 첫 번째 컬럼(숫자, 자리 맞추기용)

  • 두 번째/세 번째 컬럼에 users 테이블의 uname(아이디), pass(비밀번호)

이때 select문 두개의 컬럼 두개가 같아야 합니다. 그래서 컬럼 갯수를 강제로 맞추는 노가다로 진행해야 합니다.


훔친 아이디와 비밀번호로 로그인 해보기

성공~


이제 공격법을 봤으니 그에 맞춰서 예방법을 알아보도록 하겠습니다.

SQL Injection 예방법

Statement vs ProparedStatement

먼저 결론부터 말하자면 ProparedStatement 방식으로 쿼리를 작성해야 합니다.


Statement(정적 쿼리)

  • Statement는 SQL 쿼리문을 문자열(String)로 직접 만들어 실행하는 방식입니다.
  • 주로 아래와 같이 사용합니다.
String sql = "SELECT * FROM users WHERE username='" + username + "' AND password='" + password + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
  • 쿼리문을 코드에서 문자열로 직접 조합한다.
  • 사용자 입력값이 쿼리문에 직접 삽입 됩니다.

이런 방식으로 코드를 작성하면 위에서 다룬 예시처럼 SQL Injection 공격에 취약한 구조가 됩니다.


Propared Statement(동적 쿼리)

  • PreparedStatement는 쿼리문을 미리 컴파일하고, 실행 시 파라미터(값)를 따로 바인딩하는 방식입니다.

  • 주로 아래와 같이 사용합니다.

String sql = "SELECT * FROM users WHERE username=? AND password=?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();

이렇게 동적 쿼리문을 사용하면 쿼리문과 데이터(입력값)을 명확하게 분리해서 처리 합니다.

  • PreparedStatement는 SQL 쿼리의 구조(SELECT ... WHERE username=? AND password=?)를 먼저 데이터베이스에 전달해 "미리 컴파일".
  • 이후에 사용자가 입력한 값은 별도로 바인딩되어, 쿼리의 구조에 영향을 주지 않고 "순수한 값"으로만 전달됨
  • 즉, 설령 입력값에 악의적인 SQL 코드가 섞여 있더라도, 이 값은 단순한 문자열로 취급될 뿐, 쿼리 자체를 변조하지 못함.

Stored procedure

  • Stored Procedure(저장 프로시저)는 자주 사용하는 SQL 쿼리 로직을 데이터베이스에 미리 저장해두고, 필요할 때마다 호출해서 실행하는 방식이다.
-- 저장 프로시저 정의 (MySQL 예시)
CREATE PROCEDURE get_user_by_name(IN username VARCHAR(50))
BEGIN
    SELECT * FROM users WHERE username = username;
END;
// Java에서 호출 예시
CallableStatement cstmt = conn.prepareCall("{call get_user_by_name(?)}");
cstmt.setString(1, username);
ResultSet rs = cstmt.executeQuery();
  • 이렇게 함수 형식으로 미리 쿼리에 들어올 값과 리턴될 값을 나눠서 정의하고 입력란으로 들어오는 값을 매개인자로 넣어주며 마치 함수처럼 처리하는 방식입니다.
  • 다만, 저장 프로시저 내부에서 문자열을 조합하거나 동적으로 쿼리를 만드는 경우에는 여전히 SQL Injection 위험이 있을 수 있으므로, 항상 프로시저 내에서도 파라미터 바인딩을 지키는것이 중요합니다.

View로 접근 권한 분리하기

  • View는 데이터베이스에서 자주 사용하는 쿼리 결과를 가상의 테이블 형태로 미리 정의해 놓은 객체입니다.
  • View를 활용하면, 민감 정보나 특정 컬럼만을 노출하는 테이블을 만들어서
    애플리케이션 또는 사용자 계정별로 볼 수 있는 데이터를 제한할 수 있습니다.
CREATE VIEW user_public_view AS
SELECT id, name, email FROM users;

-- 일반 유저 계정에만 뷰 접근 권한 부여
GRANT SELECT ON user_public_view TO 'normal_user'@'%';
-- 원본 테이블(users)에는 권한 부여하지 않기
REVOKE ALL PRIVILEGES ON users FROM 'normal_user'@'%';

즉 테이블을 볼 수 있는 계정에 제한을 걸어주는 방법입니다. 계정 권한에 대한 내용은 아래에서 다시 다루도록 하겠습니다.


ORM(Object Relational Mapping)

  • ORM이란 객체 지향 언어(예: Java)에서 데이터베이스의 데이터를 객체로 매핑해서 다루게 해주는 기술입니다.
    대표적으로 Java에서는 JPA, Hibernate, MyBatis(Mapper) 등을 사용합니다.

  • ORM을 사용하면 직접 SQL 쿼리를 작성하지 않고,
    메서드 호출이나 JPQL(Java Persistence Query Language) 등으로 데이터를 조회/저장할 수 있습니다.

// 예시: JPA Repository를 이용한 사용자 조회
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsernameAndPassword(String username, String password);
}

// 실제 서비스 코드에서
Optional<User> user = userRepository.findByUsernameAndPassword(username, password);
  • 이처럼 JPA의 메서드 파라미터에 사용자 입력값을 전달하면,
    JPA 내부에서 쿼리문과 값(파라미터)을 명확히 분리해서 데이터베이스에 전달합니다.

  • 동적 쿼리문과 마찬가지로 실제 입력값이 쿼리에 직접 삽입 되는 것이 아니라, 내부적으로 동적쿼리처럼 쿼리와 파라미터가 분리되어서 처리됩니다.


DB 권한 및 계정 관리

데이터베이스의 계정과 권한 관리는 실무에서 가장 기본적이면서도 중요한 보안 요소입니다.
적절한 권한 분리와 관리 없이는, 내부자에 의한 정보 유출이나 실수, 외부 공격 시 대규모 피해로 이어질 수 있습니다.


최소 권한의 원칙(Principle of Least Privilege)

  • 각 DB 계정에는 업무상 꼭 필요한 최소한의 권한만을 부여해야 합니다.

  • 예를 들어, 단순 조회 서비스라면 SELECT만, 데이터 입력 서비스라면 INSERT/UPDATE만, 운영 자동화 계정에는 DDL 권한을 부여하지 않는 것이 안전합니다.

  • DDL(테이블 구조 변경), DCL(권한 부여/회수) 권한은 반드시 관리자 계정에만 부여 해야 합니다.

-- 조회 전용 계정
GRANT SELECT ON mydb.* TO 'readonly_user'@'%';

-- 데이터 입력/수정만 가능한 계정
GRANT INSERT, UPDATE, DELETE ON mydb.orders TO 'order_manager'@'%';

-- 필요 없는 권한은 반드시 회수
REVOKE ALL PRIVILEGES ON mydb.* FROM 'readonly_user'@'%';

DB 권한 부여/회수 예시 정리

구분SQL 예시설명
조회 권한 부여GRANT SELECT ON mydb.* TO 'readonly_user'@'%';전체 DB(mydb) 조회만 가능
데이터 입력/수정GRANT INSERT, UPDATE ON mydb.orders TO 'order_manager'@'%';특정 테이블(orders)에 입력/수정만 가능
DDL 권한 부여GRANT CREATE, ALTER ON mydb.* TO 'admin_user'@'%';DB 구조 변경 권한(테이블 생성/수정 등) 부여
권한 회수REVOKE ALL PRIVILEGES ON mydb.* FROM 'readonly_user'@'%';모든 권한 회수
특정 권한 회수REVOKE INSERT, UPDATE ON mydb.orders FROM 'order_manager'@'%';테이블(orders) 입력/수정 권한만 회수
특정 뷰만 허용GRANT SELECT ON user_public_view TO 'normal_user'@'%';뷰(user_public_view)에만 SELECT 허용
원본 테이블 차단REVOKE ALL PRIVILEGES ON users FROM 'normal_user'@'%';users 테이블 직접 접근은 차단

정리

  • SQL 쿼리 작성 시에는 항상 파라미터 바인딩(PreparedStatement, ORM 등)을 기본 원칙으로 삼고,
  • 각 계정에 꼭 필요한 최소한의 권한만 부여하며,
  • View, Stored Procedure 등 DB 자체 기능도 적극 활용해서 정보 노출을 최소화해야 합니다.

SQL Injection 공격에 사용된 사이트

주석 처리 공격
UNION SELECT 이용


Reference

profile
개발에 대한 고민과 성장의 기록을 일기장처럼 성찰하며 남기는 공간

1개의 댓글

comment-user-thumbnail
2025년 7월 8일

좋은 글 감사합니다

답글 달기