설명
SQL 인젝션은 사용자 입력값이 그대로 SQL 쿼리 문자열에 연결(concatenation)되거나 포맷팅되어 실행될 때 발생하는 취약점입니다. 개발자가 "SELECT ... WHERE id = " + userInput 과 같이 직접 쿼리를 문자열로 조립하면, 공격자는 1 OR 1=1, 1; DROP TABLE users-- 같은 값을 넣어 쿼리 구조를 조작할 수 있습니다. DB 권한, 드라이버와 설정에 따라 인증 우회, 데이터 조회·변경·삭제로 이어질 수 있습니다. 여러 SQL 문을 한 번에 실행할 수 있는지도 드라이버와 설정에 따라 다릅니다.
잠재적 영향
- 데이터 조회 및 유출 (정보 노출): 공격자가 원래 볼 수 없는 다른 사용자의 개인정보, 계정 정보, 내부 설정값 등을 조회할 수 있습니다.
- 데이터 변조 및 삭제 (데이터 무결성 훼손): INSERT, UPDATE, DELETE, DROP 등의 명령을 악용해 데이터 조작, 대량 삭제, 테이블/스키마 삭제가 가능합니다.
- 인증/인가 우회 (권한 우회):
OR 1=1등의 조건을 주입하여 로그인 우회, 권한 체크 우회가 발생할 수 있습니다. - 서비스 장애 (서비스 거부): 대량 쿼리, 무한 루프성 쿼리, 대규모 삭제 등을 실행해 DB와 서비스 전체에 장애를 유발할 수 있습니다.
해결 방법
- 매개변수화된 쿼리 사용:
db.Query,db.QueryRow,db.Exec에 드라이버가 지원하는?,$1등의 placeholder를 사용하고 값을 별도 인자로 전달하세요. - ORM이나 Query Builder도 값 바인딩 API를 사용하고, 사용자 입력을 raw SQL 문자열에 연결하지 마세요.
- ID나 숫자는
strconv.Atoi등으로 타입을 검증하고 변환 실패를 처리하세요. - 컬럼명이나 정렬 기준처럼 바인딩할 수 없는 SQL 요소는 코드에서 정한 허용 목록으로만 선택하세요.
- DB 계정에 불필요한
DROP,ALTER등의 권한을 주지 마세요. - 상세 DB 오류는 보호된 내부 로그에 남기고 사용자에게는 일반적인 오류만 반환하세요.
예시
SQL 매개변수 전달을 비교하는 핸들러 발췌입니다. ?를 지원하는 드라이버를 전제로 하며, 실제 드라이버에 맞는 placeholder를 사용하세요. 인증·인가와 응답 출력 처리는 별도로 구성해야 합니다.
변경 전
go
package main
import (
"database/sql"
"net/http"
)
// 취약한 핸들러 예시
func unsafeUserDetailHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 쿼리 파라미터로부터 id를 문자열 그대로 가져옴
userID := r.URL.Query().Get("id")
// 비추천: 사용자 입력을 직접 문자열로 이어붙여 SQL 생성
// 예: /user?id=1 OR 1=1 --
query := "SELECT name, email FROM users WHERE id = " + userID
row := db.QueryRow(query)
var name, email string
if err := row.Scan(&name, &email); err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
w.Write([]byte(name + " / " + email))
}
}
변경 후
go
package main
import (
"database/sql"
"net/http"
"strconv"
)
// 안전한 핸들러 예시 (Prepared Statement 사용)
func safeUserDetailHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userIDStr := r.URL.Query().Get("id")
// 1. 타입 검증: 숫자가 아니면 에러 처리
userID, err := strconv.Atoi(userIDStr)
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
// 2. Prepared Statement / placeholder 사용
// - `?` 또는 `$1` 등에 값이 바인딩되므로
// 문자열 조작으로 쿼리 구조를 바꿀 수 없음
const query = "SELECT name, email FROM users WHERE id = ?"
row := db.QueryRow(query, userID)
var name, email string
if err := row.Scan(&name, &email); err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Write([]byte(name + " / " + email))
}
}
설명:
- 변경 전:
id가 SQL 문자열에 직접 연결되어 조건을 바꿀 수 있습니다.OR 1=1로 더 많은 행이 선택될 수 있지만, 이QueryRow호출은 첫 번째 행만 읽고 나머지는 버립니다. 여러 문을 실행하는 공격은 드라이버 설정과 DB 권한에 따라 달라집니다. - 변경 후:
strconv.Atoi로 타입을 확인하고, 고정된 SQL에userID를 별도 인자로 전달해 데이터가 SQL 문법으로 해석되지 않게 합니다. 실제 오류 처리에서는sql.ErrNoRows와 DB 장애를 구분하세요.