RDBMS 형식의 데이터베이스는 데이터를 관리하는데 '쿼리문'을 사용합니다. 기능을 구현할 때 통상 프론트엔드 혹은 백엔드에서 사전에 쿼리문을 작성해두고, 특정 필드에 사용자가 입력한 값을 포함시켜 데이터베이스로 완성된 쿼리문을 전달하는 방식이 사용되곤합니다.
SQL 인젝션 공격이란, 사용자 입력값에 공격을 위한 '쿼리문'을 입력하여 개발자가 의도하지않은 방식으로 데이터베이스의 정보를 유출시키거나 데이터베이스 자체를 무단으로 조작하는 웹 보안 위협의 일종입니다.
예를 들어, 웹사이트에 사용자 로그인 기능이 있다고 가정해봅시다. 사용자는 자신의 아이디와 비밀번호를 입력하여 로그인합니다. 웹 어플리케이션은 이 정보를 SQL 쿼리에 사용하여 사용자를 인증합니다.
대략 아래와 같은 SQL 쿼리가 있다고 가정해봅시다:
SELECT * FROM users WHERE username = '[사용자 입력 아이디]' AND password = '[사용자 입력 비밀번호]';
통상적으로 사용자는 자신의 아이디와 비밀번호를 입력하고, 이는 쿼리문에 존재하는 필드에 주입되어 데이터베이스로 전송됩니다.
입력한 아이디와 비밀번호가 일치하는 사용자가 데이터베이스에 존재하는지의 대한 결과가 백엔드로 반환되는 것이 개발자가 의도한 동작 방식입니다.
그런데 공격자가 아래와 같은 값을 입력했을 때, 개발자가 의도하지 않는 상황이 발생하게 됩니다.
아이디: admin
비밀번호: '' OR '1'='1'
이 경우 완성된 SQL 쿼리문은 아래와 같은 형태가 됩니다.
SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1';
사용자가 잘못된 비밀번호를 입력했다면 일치하는 사용자가 존재하지 않기 때문에 데이터베이스는 아무런 사용자 데이터도 반환하지 않습니다. 그런데 '1'='1'은 언제나 true에 해당되는 값이기 때문에, 비밀번호의 일치 여부를 따지지 않고 데이터베이스에 존재하는 모든 사용자의 데이터가 반환되고 맙니다. 결과적으로 공격자는 admin 계정의 비밀번호를 알지도 못하지만, 성공적으로 admin 계정에 로그인하게 되는 것입니다.
이와 같은 방식으로, 공격자는 데이터베이스 쿼리의 논리를 변경하여 민감한 정보에 접근하거나 데이터베이스 시스템을 조작할 수 있습니다. 이러한 공격을 방지하기 위해서는 사용자 입력을 적절히 검증하고, Prepared Statements와 같은 안전한 방법으로 SQL 쿼리를 구성해야 합니다.
SQL 인젝션을 방지하기 위한 몇 가지 주요 전략은 다음과 같습니다:
애플리케이션 내에서 SQL 쿼리를 작성할 때, 사용자의 입력을 쿼리에 직접 삽입하는 것이 아니라, Prepared Statements를 사용합니다.
Prepared Statements는 SQL 인젝션 방지에서 가장 중요한 방법 중 하나입니다. 이 방법은 SQL 쿼리를 미리 컴파일하고, 사용자 입력을 쿼리의 매개변수로 전달합니다. 이렇게 하면 사용자 입력이 SQL 쿼리의 구조를 변경할 수 없습니다.
const mysql = require('mysql');
const connection = mysql.createConnection({
// 데이터베이스 설정
});
connection.connect();
// Prepared Statement 사용
const user_input = '사용자 입력값';
const sql = 'SELECT * FROM users WHERE username = ?';
connection.query(sql, [user_input], (error, results) => {
if (error) throw error;
// 결과 처리
});
connection.end();
'DROP TABLE users;--'와 같은 SQL 인젝션 공격이 실행되었다고 가정해봅시다. Prepared Statements가 적용된 상태라면, 'DROP TABLE users;--'는 단순한 username 필드의 값으로 간주되어 쿼리의 다른 부분으로 주입되지 않습니다.
모든 사용자 입력을 검증하여, 잠재적인 해를 끼칠 수 있는 데이터를 필터링합니다.
사용자로부터 받는 모든 입력은 검증되어야 합니다. 이는 SQL 인젝션 뿐만 아니라 다른 형태의 공격을 방지하는 데에도 중요합니다. 입력 살균은 스크립트, SQL 구문과 같이 위험할 수 있는 문자들을 제거하거나 변환하는 과정을 말합니다.
악의적인 공격자가 입력 폼에 '; DROP TABLE users;--' 같은 문자열을 입력한다면, 이는 데이터베이스 쿼리를 변경하여 사용자 테이블을 삭제하는 치명적인 명령을 실행할 수 있습니다.
function validateInput(input) {
const dangerousCharacters = /['";--]/g;
return input.replace(dangerousCharacters, '');
}
// 사용자 입력 검증
const userInput = "'; DROP TABLE users;--";
const safeInput = validateInput(userInput);
이 예시에서 validateInput 함수는 이러한 위험한 문자열을 제거함으로써 SQL 인젝션 공격을 방지합니다. 예를 들어, 위에서 언급한 입력값 '; DROP TABLE users;--'에서 ';--'와 같은 SQL 명령을 구분하는 특수 문자들을 제거하거나 대체합니다. 그 결과, 이 입력값은 더 이상 데이터베이스 쿼리를 변경할 수 없는 안전한 형태로 변환됩니다.
데이터베이스 사용자에게 필요한 최소한의 권한만을 부여합니다.
데이터베이스 계정에 최소한의 권한만 부여합니다. 예를 들어, 어떤 서비스에서는 데이터를 읽기만 하면 충분할 수 있습니다. 이 경우, 해당 계정에는 읽기 권한만 부여합니다. 이는 만약 SQL 인젝션 공격이 발생하더라도 그 피해를 최소화할 수 있게 합니다.
데이터베이스 오류 메시지를 사용자에게 직접 보여주지 않음으로써, 공격자에게 유용한 정보를 제공하지 않습니다.
데이터베이스에서 발생하는 에러 메시지를 사용자에게 그대로 보여주지 않아야 합니다. 에러 메시지는 공격자에게 데이터베이스 구조에 대한 힌트를 줄 수 있습니다.
connection.query(sql, [user_input], (error, results) => {
if (error) {
console.error('Database query error occurred');
return;
}
// 결과 처리
});