모바일 개발에서의 SQL Injection: 개념, 공격 방법 및 보호

저자: IT Sectr 게시일: 2026-04-06 읽는 시간: 9 분

SQL Injection은 공격자가 쿼리 파라미터에 악성 SQL 코드를 주입하여 데이터에 대한 무단 액세스나 데이터 수정 능력을 얻는 데이터베이스 공격 유형입니다. OWASP(2025)에 따르면, SQL Injection은 데이터베이스를 완전히 손상시킬 수 있는 중요한 취약점 중 하나로 남아 있습니다. SQL 코드 인젝션을 통해 공격자는 레코드를 읽고, 수정하고, 삭제할 수 있으며, 경우에 따라 서버의 운영 체제에 액세스할 수 있습니다.

핵심 사항

  • SQL Injection — 사용자 파라미터를 통해 SQL 코드를 주입하여 데이터베이스 쿼리 로직을 변경
  • 쿼리 파라미터화 — prepared statements를 사용하여 SQL 코드를 데이터와 분리하는 주요 보호 방법
  • 공격 유형 — 클래식 인젝션(WHERE 통해), Blind SQLi(논리적 추론), UNION 기반(다른 테이블 읽기)
  • Error-based SQLi — 데이터베이스 오류 메시지를 통한 데이터 추출
  • ORM 프레임워크 — 올바르게 사용하면 SQLi 위험을 줄이지만 완전히 제거하지는 않음

SQL Injection이란?

SQL Injection은 애플리케이션이 사용자 데이터와 문자열 연결을 통해 SQL 쿼리를 구성할 때 발생하는 취약점입니다. 공격자는 특수하게 조작된 문자열을 쿼리 파라미터에 전달하여 SQL 명령의 구조를 변경합니다. 주입된 문자열은 데이터가 아닌 SQL 코드의 일부가 되어, 공격자가 데이터베이스에 대해 임의의 쿼리를 실행할 수 있게 합니다. Verizon 데이터 유출 보고서(2025)에 따르면, SQL Injection은 조사된 모든 데이터 유출의 8%에서 발견됩니다.

SQL Injection이 여전히 중요한 이유

잘 알려진 취약점임에도 불구하고(1990년대 후반에 처음 언급됨), SQL Injection은 현대 애플리케이션에서 여전히 발견됩니다. 그 이유는 인적 오류입니다. 개발자가 문자열 연결을 사용하는 코드를 작성하고, 레거시 코드가 리팩토링되지 않으며, ORM 프레임워크가 부적절하게 사용됩니다(예: 문자열 보간을 사용한 raw 쿼리). Veracode(2026)에 따르면, 스캔된 모든 애플리케이션의 약 14%에 최소 하나의 SQLi 취약점이 포함되어 있습니다.

공격자는 무엇을 할 수 있나?

성공적인 SQL Injection은 공격자에게 광범위한 능력을 제공합니다: 비밀번호 해시와 사용자 개인 데이터를 포함한 모든 데이터베이스 테이블 읽기, 레코드 수정 및 삭제, 관리 작업(DROP TABLE, TRUNCATE) 실행, 일부 구성에서는 xp_cmdshell(MSSQL) 또는 INTO OUTFILE(MySQL)을 통한 원격 명령 실행. 결과는 사용자 데이터 유출부터 시스템 제어권 완전 상실까지 다양합니다.

SQL 인젝션의 유형

SQL 인젝션은 데이터베이스에서 데이터를 추출하는 방법에 따라 분류됩니다. 방법 선택은 애플리케이션이 쿼리 결과와 오류 메시지를 처리하는 방식에 따라 달라집니다. OWASP 분류는 세 가지 주요 유형을 식별합니다: In-band(동일한 채널을 통한 데이터 추출), Inferential/Blind(논리적 추론), Out-of-band(다른 채널을 통한 데이터 전송).

유형데이터 추출 방법복잡성빈도
In-band(클래식)쿼리 결과를 직접 통해낮음높음
Blind SQLi서버 응답에서 논리적 추론높음중간
Out-of-band외부 채널(DNS, HTTP)을 통해중간낮음

In-band SQL Injection(클래식)

가장 일반적인 유형입니다. 공격자는 쿼리 파라미터에 SQL 코드를 주입하고, 그 결과가 서버 응답에 직접 표시됩니다. 두 가지 하위 유형: Error-based(DB 오류 메시지를 통해)와 UNION-based(UNION SELECT 연산자를 통해). Error-based는 테이블 이름이나 쿼리 구조를 드러낼 수 있는 MySQL 구문 오류와 같은 오류 메시지의 정보를 활용합니다. UNION-based는 합법적인 쿼리 결과를 데이터베이스의 다른 테이블 데이터와 결합할 수 있게 합니다.

Blind SQL Injection(블라인드)

애플리케이션이 쿼리 결과나 오류 메시지를 표시하지 않을 때 사용됩니다. 공격자는 논리적 조건이 포함된 쿼리를 보내고 서버 응답의 차이(응답 시간 또는 페이지 내용 등)를 분석하여 예/아니오 질문을 합니다. Time-based Blind SQLi는 조건을 확인하기 위해 지연 함수(SLEEP, WAITFOR DELAY)를 사용합니다. 조건이 참이면 페이지 로딩 시간이 더 오래 걸립니다. 이 방법은 매우 느려서 하나의 레코드를 추출하는 데 몇 시간이 걸릴 수 있습니다.

python
# Blind SQL Injection 예제(시간 기반)
# SQLi에 취약한 경우, 조건이 충족되면 SLEEP(2)이 실행됨
import requests
import time

payload = "' OR IF(SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a',SLEEP(2),0) -- "
url = "http://example.com/user?id=1" + payload

start = time.time()
response = requests.get(url)
elapsed = time.time() - start

# 응답이 >2초 후에 도착하면 — 비밀번호의 첫 글자는 'a'
print(f"Time: {elapsed:.2f}s - First letter is 'a'" if elapsed > 2 else "First letter is not 'a'")

Out-of-band SQL Injection

데이터는 HTTP 응답이 아닌 대체 채널(DNS 쿼리, 외부 서버로의 HTTP 요청, SMTP)을 통해 전송됩니다. 애플리케이션이 쿼리 결과를 반환하지 않거나 오류를 표시하지 않을 때 사용됩니다. MySQL은 DNS 쿼리를 트리거할 수 있는 LOAD_FILE() 함수를 지원하며, MSSQL에는 원격 SMB 서버로 데이터를 보내는 xp_dirtree가 있습니다. Out-of-band SQL Injection은 효과적이지만 추가적인 공격자 서버 설정과 특정 DB 함수가 필요합니다.

SQL 인젝션의 작동 방식

SQL Injection 메커니즘은 SQL이 문자열 리터럴에 따옴표를 사용한다는 점에 기반합니다. 애플리케이션이 사용자 입력을 이스케이프 없이 SQL 쿼리에 직접 삽입하는 경우, 공격자는 문자열을 “닫고” 임의의 SQL 코드를 추가할 수 있습니다. 예를 들어, 쿼리 SELECT * FROM users WHERE name = '$input'' OR '1'='1을 삽입하면 SELECT * FROM users WHERE name = '' OR '1'='1'로 변환되어 모든 사용자를 반환합니다.

클래식 예제: 인증 우회

쿼리 SELECT * FROM users WHERE username = '$user' AND password = '$pass'를 사용하는 로그인 폼을 생각해보세요. 공격자가 사용자 이름 필드에 admin' -- 을 입력하고 비밀번호를 비워두면, 결과 쿼리는 SELECT * FROM users WHERE username = 'admin' -- ' AND password = ''이 됩니다. -- 문자는 나머지 쿼리를 주석 처리하여 비밀번호 확인을 비활성화합니다. 서버는 admin 사용자 레코드를 반환하고, 공격자는 비밀번호를 모르고 로그인합니다.

python
# SQL Injection 예제 — 인증 우회
# 취약한 코드: 직접 문자열 연결
def login_vulnerable(username, password):
    query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
    cursor.execute(query)
    return cursor.fetchone() is not None

# username = "admin' --"이 비밀번호 확인을 무효화
# 안전한 버전: 파라미터화된 쿼리
def login_secure(username, password):
    query = "SELECT * FROM users WHERE username = %s AND password = %s"
    cursor.execute(query, (username, password))
    return cursor.fetchone() is not None

UNION-based: 다른 테이블 읽기

UNION SELECT 연산자는 두 SELECT 쿼리의 결과를 결합할 수 있습니다. 공격자가 취약한 파라미터를 찾으면 다른 테이블의 쿼리와 함께 UNION SELECT를 추가합니다. 예: ' UNION SELECT username, password FROM admins --. 성공 조건은 두 쿼리의 컬럼 수가 일치해야 한다는 것입니다. 컬럼 수는 ORDER BY를 통해 결정됩니다(' ORDER BY 1--, 그 다음 2, 3... 오류가 발생할 때까지). 컬럼 수를 알면 공격자는 동일한 필드 수로 UNION SELECT를 삽입합니다.

모바일 애플리케이션에서의 SQL Injection

모바일 애플리케이션은 서버 측(API)과 클라이언트 측(로컬 데이터베이스: SQLite, Realm) 모두에서 SQL Injection에 직면합니다. 모바일 API의 서버 측 SQLi는 웹 애플리케이션과 유사하지만, 로컬 데이터베이스는 추가적인 벡터를 만듭니다. 애플리케이션이 SQLite에 데이터를 저장하고 문자열 연결로 쿼리를 실행하는 경우, API를 통해 로컬 데이터베이스에 유입된 악성 데이터가 후속 처리 중에 SQLi를 트리거할 수 있습니다.

Android/iOS의 SQLite에서 SQL Injection

디바이스의 로컬 SQLite 데이터베이스도 애플리케이션이 문자열 연결로 쿼리를 구성하는 경우 SQL Injection에 취약합니다. Android의 Content Providers와 iOS의 Core Data는 기본적으로 파라미터화를 사용하지만, raw 쿼리는 개발자의 주의가 필요합니다. SQLite는 세미콜론으로 구분된 여러 쿼리를 지원하지 않아 공격자의 옵션을 제한하지만 WHERE 조건을 통한 데이터 읽기를 막지는 못합니다. Android에서는 항상 selectionArgs를, iOS에서는 파라미터와 함께 NSPredicate를 사용하세요.

모바일 애플리케이션 API를 통한 SQL Injection

모바일 애플리케이션이 통신하는 API는 다른 웹 서버와 마찬가지로 취약합니다. 모바일 개발자는 종종 SQLi를 백엔드 문제로만 생각하지만, 취약점은 클라이언트의 파라미터를 수락하는 API 엔드포인트에서 발생합니다. 책임 분리는 보호가 되지 않습니다. 백엔드 개발자가 쿼리 파라미터화를 잊은 경우, 사용자의 모바일 앱이 공격 벡터가 됩니다. 백엔드에 ORM 또는 prepared statements 사용을 요구하세요.

kotlin
// Android의 로컬 SQLite에서 SQL Injection 예제
// 취약한 코드: 직접 연결
fun getUserVulnerable(db: SQLiteDatabase, userId: String): Cursor {
    return db.rawQuery(
        "SELECT * FROM users WHERE id = $userId", null
    )
}

// 안전한 버전: selectionArgs를 통한 파라미터화
fun getUserSecure(db: SQLiteDatabase, userId: String): Cursor {
    return db.rawQuery(
        "SELECT * FROM users WHERE id = ?",
        arrayOf(userId)
    )
}

SQL Injection 방지 방법

SQL Injection으로부터의 보호는 간단한 원칙에 기반합니다: SQL 쿼리에서 사용자 입력을 절대 신뢰하지 마십시오. 유일하게 신뢰할 수 있는 방법은 SQL 코드와 데이터를 별도로 전달하는 쿼리 파라미터화(prepared statements)입니다. 이스케이프, 검증, WAF 등 다른 모든 방법은 추가적인 보안 계층이지만 파라미터화를 대체하지는 않습니다. OWASP(2025)에 따르면, 파라미터화는 SQL Injection 공격의 100%를 방지합니다.

Prepared Statements(파라미터화된 쿼리)

Prepared statements를 사용하면 SQL 쿼리가 먼저 DB 서버에 의해 데이터 없이 컴파일된 후, 파라미터 값이 별도로 전달됩니다. 데이터베이스는 파라미터를 실행 가능한 코드가 아닌 데이터로 취급합니다. 공격자가 ' OR '1'='1을 전달하더라도 데이터베이스는 이를 SQL 코드가 아닌 문자열 값으로 해석합니다. Prepared statements는 PHP의 PDO, Java의 PreparedStatement, Python의 cursor.execute 등 모든 최신 언어와 프레임워크에서 지원됩니다.

ORM 프레임워크와 쿼리 빌더

최신 ORM(Hibernate, Entity Framework, SQLAlchemy, Room)은 개발자가 raw 쿼리로 전환하지 않는 한 쿼리 실행 시 자동으로 파라미터화를 사용합니다. 그러나 ORM이 완전히 보호하지는 않습니다. JPA의 @Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true)와 같은 구조는 연결이 아닌 명명된 파라미터를 통해 파라미터를 전달해야 합니다. 쿼리 빌더(Knex, jOOQ)도 raw 메서드를 사용하지 않으면 기본적으로 쿼리를 파라미터화합니다.

문자열 이스케이프(주요 방법으로 권장되지 않음)

특수 문자 이스케이프(mysql_real_escape_string)는 모든 유형의 SQL Injection으로부터 보호하지 않는 구식 방법입니다. 문제는 이스케이프가 인코딩에 의존하며 멀티바이트 인코딩(예: 아시아 시스템의 GBK)을 사용할 때 우회될 수 있다는 점입니다. 파라미터화가 불가능한 레거시 코드에서만 이스케이프를 사용하고, 항상 엄격한 입력 유형 검증과 함께 사용하세요.

방법효과권장
Prepared Statements100%모든 쿼리에 필수
ORM(올바른 사용)99%권장
문자열 이스케이프70%(인코딩에 따라 다름)레거시만
입력 검증(화이트리스트)50%(숫자만)추가
WAF(Web Application Firewall)60%추가

데이터베이스 최소 권한 원칙

애플리케이션 계정에는 최소한의 필요한 권한(SELECT, INSERT, UPDATE, DELETE — 애플리케이션이 실제로 필요로 하는 테이블만)이 있어야 합니다. 애플리케이션 계정의 DROP, TRUNCATE, CREATE 사용을 금지하세요. 이는 SQL Injection이 성공하더라도 피해를 제한합니다. 공격자는 테이블을 삭제하거나 관리 작업을 수행할 수 없습니다.

SQL 인젝션 탐지 도구

정기적인 SQL Injection 테스트는 보안 개발 파이프라인의 일부여야 합니다. 정적 분석, 동적 스캔 및 수동 침투 테스트의 조합이 최상의 결과를 제공합니다. Synopsys 사이버 보안 보고서(2025)에 따르면, 자동화된 스캐너는 SQLi 취약점의 최대 70%를 찾지만, 복잡한 Blind 공격은 수동 테스트가 필요합니다.

  • sqlmap — SQL Injection의 자동 탐지 및 악용을 위한 가장 인기 있는 도구
  • OWASP ZAP — 활성 SQLi 스캔 모듈을 갖춘 무료 DAST 스캐너
  • Burp Suite Scanner — 자동 SQL Injection 탐지 기능을 갖춘 전문 도구
  • SonarQube — SQLi 패턴을 포함한 취약점에 대한 정적 코드 분석
  • CodeQL — 소스 코드에서 SQL 인젝션을 찾기 위한 시맨틱 코드 분석

모바일 애플리케이션의 경우 로컬 SQLite 분석도 중요합니다: 모든 rawQuery 호출, ContentProvider 쿼리 및 rawQuery를 사용한 Room 쿼리를 확인하세요. 도구: Android Studio Lint(SQLite에서 SQLi 탐지), APK/IPA의 자동 정적 및 동적 분석을 위한 MobSF(Mobile Security Framework). 또한 모바일 애플리케이션 트래픽의 프록시 인터셉션과 함께 sqlmap을 통해 API 엔드포인트를 테스트하는 것이 좋습니다.

자주 묻는 질문

SQL Injection과 NoSQL Injection의 차이점은 무엇인가요?

SQL Injection은 SQL 쿼리를 통해 관계형 데이터베이스를 공격합니다. NoSQL Injection은 쿼리 연산자($gte, $ne, $where)를 통해 비관계형 데이터베이스(MongoDB, Couchbase)에 영향을 줍니다. MongoDB에서는 애플리케이션이 JSON 문자열에서 BSON 문서를 구성하는 경우 인젝션이 가능합니다. 보호 메커니즘은 유사합니다: 파라미터화와 유형 검증입니다.

코드에서 SQL Injection을 직접 어떻게 탐지하나요?

사용자 데이터와 문자열 연결을 통해 SQL 쿼리가 형성되는 모든 위치를 찾으세요. "SELECT ... WHERE id = " + userId 또는 f"UPDATE ... SET name = '{name}'"과 같은 패턴을 찾으십시오. 이러한 각 줄은 잠재적인 SQL 인젝션입니다. 모두 파라미터화된 쿼리 또는 prepared statements로 교체하세요.

ORM이 자동으로 SQL Injection을 방지하나요?

ORM 프레임워크는 쿼리 빌더 메서드명명된 파라미터를 사용하는 경우에만 자동으로 보호합니다. raw 쿼리(JPA의 nativeQuery, Room의 rawQuery)를 사용하는 경우 보호가 작동하지 않습니다. 문자열 연결이 아닌 준비된 표현식을 통해 파라미터를 전달해야 합니다.

Second-Order SQL Injection이란 무엇인가요?

Second-Order SQL Injection은 악성 데이터가 데이터베이스에 안전하게 저장된 후, 이스케이프 없이 다른 쿼리에서 사용되는 공격입니다. 예를 들어, 공격자가 ' OR '1'='1과 같은 사용자 이름으로 등록합니다. 데이터는 문자열로 저장되며, 등록 시에는 공격이 없습니다. 그러나 다른 쿼리가 파라미터화 없이 SQL에서 해당 사용자 이름을 사용하면 인젝션이 트리거됩니다.

NoSQL Injection이 SQL Injection보다 더 위험할 수 있나요?

NoSQL Injection은 개발자 인식 부족으로 인해 잠재적으로 더 위험할 수 있습니다. 개발자는 SQL Injection에 대해 알고 있고 대부분 ORM을 사용하지만, NoSQL Injection에 대해 아는 사람은 거의 없습니다. MongoDB에서 부적절하게 형성된 쿼리는 컬렉션의 모든 문서를 반환할 수 있습니다. 보호는 동일합니다: prepared statements(BSON 파라미터화)와 엄격한 입력 검증입니다.

요약

  • SQL Injection — DB 쿼리에서 이스케이프되지 않은 사용자 파라미터를 통해 SQL 코드를 주입하는 공격
  • 주요 유형 — In-band(클래식), Blind(블라인드, 시간 기반), Out-of-band(외부 채널 통해)
  • 쿼리 파라미터화 — SQL Injection에 대한 유일한 100% 신뢰할 수 있는 보호 방법
  • ORM 신화 — ORM은 내장 메서드 사용 시에만 보호, 연결을 사용한 raw 쿼리는 여전히 취약
  • 최소 권한 원칙 — DB 계정 권한 제한이 성공적인 공격 시 피해 최소화
  • 정기적 테스트 — sqlmap, OWASP ZAP, SonarQube를 CI/CD 파이프라인에 포함
  • 로컬 SQLite — 모바일 앱도 로컬 DB 쿼리를 파라미터화해야 함

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기