생활코딩/WEBn

[Node.js & MySQL] 보안: SQL injection, Escaping

hayjo 2021. 1. 13. 20:41

외부로부터 유입되는 정보는 필터링이 필요하다.

유저가 입력 데이터로 악성 SQL을 주입해 공격하는 경우인 SQL injection 예방법과

해당 데이터를 웹브라우저로 실행할 때 발생할 수 있는 Cross site scripting (XSS) 예방법을 살펴본다.

 

[강의 출처] opentutorials.org/course/3347/21299

 

SQL injection 예방법

페이지를 아래처럼 로드한다고 할 때,

var mysql = require('mysql');

var db = mysql.createConnection({
	  host     : 'localhost',
	  user     : 'root',
	  password : 'password',
	  database : 'data'
	})
db.connect();

var app = http.createServer(function(request, response){
    var _url = request.url;
    var id = url.parse(_url, true).query.id;
// 여기까지 설정


    db.query('SELECT * FROM author WHERE id=?',[id], function(err, data){
      // 실제 실행, id는 위에서 URL의 쿼리부분을 읽어온 값이다
    }

id='1'인 경우 실제 SQL문은 아래처럼 생성된다.

SELECT * FROM author WHERE id = '1'

공격자가 URL에 입력된 쿼리 부분이 그대로 SQL문에 전달된다는 사실을 이용해서, 아래와 같은 URL을 요청했다고 하면,

https://testpage.com/?id=1;DROP TABLE tablename;

이런 SQL문이 생성된다. SQL은 세미콜론으로 명령을 구분하지만, 전체 인풋이 텍스트로 묶여있기 때문에 해당 공격은 실행되지 않는다.

SELECT * FROM author WHERE id = '1;DROP TABLE tablename;'

 

그 이유는 db.query를 호출할 때 1번째 인자로 쿼리문('SELECT * FROM author WHERE id=?')을,

2번째 인자로 입력데이터(id)를 주어서 mysql '?'를 변수로 치환 후 처리했기 때문이다.

따라서 가능하면 ? 치환형태로 쿼리문을 실행하는 것이 좋다.

 

이것이 가능한 이유는 mysql 모듈에서 기본적으로 SQL Injection(입력 데이터로 악성 SQL을 주입해 공격하는 방법)을 예방하기 위한 처리를 해두었기 때문이다.

 

 

*도큐먼트의 해당 부분에 아래와 같이 설명되어 있다.

In order to avoid SQL Injection attacks, you should always escape any user provided data before using it inside a SQL query. You can do so using the mysql.escape(), connection.escape() or pool.escape() methods:

SQL문 삽입을 통한 공격을 피하기 위해서는, 유저가 입력한 데이터를 SQL쿼리에 사용하기 전에 항상 치환해야한다. mysql.escape(), connection.escape(), pool.escape() 함수를 이용할 수 있다.

var userId = 'some user provided value';
var sql    = 'SELECT * FROM users WHERE id = ' + connection.escape(userId);  // 이스케이프가 적용되었다
connection.query(sql, function (error, results, fields) {
  if (error) throw error;
  // ...
});

Alternatively, you can use ? characters as placeholders for values you would like to have escaped like this:

대안으로 ? 문자를 써서 치환하고자 하는 값의 플레이스홀더로 사용할 수 있다:

connection.query('SELECT * FROM users WHERE id = ?', [userId], function (error, results, fields) {
  if (error) throw error;
  // ...
});

그리고 데이터타입에 따라서 어떻게 치환되는지에 대해서도 설명이 나와 있는데,

숫자는 그대로, 불리언값은 true/false, 데이터는 'YYYY-mm-dd HH:ii:ss' 형태, undefined, null은 NULL 등등이 있다.

그 중 유용할 듯한 부분은 객체는 key = 'val'로 치환되는 부분.

var post  = {id: 1, title: 'Hello MySQL'};
var query = connection.query('INSERT INTO posts SET ?', post, function (error, results, fields) {
  if (error) throw error;
  // Neat!
});
console.log(query.sql); // INSERT INTO posts SET `id` = 1, `title` = 'Hello MySQL'

객체를 넣으면 알아서 변환해준다! 정말이지 좋은 모듈이다.

 

혹시 이 방법을 몰랐다고 하더라도, 다행히 mysql 모듈측에서 connection.query()로 실행될 수 있는 구문 수를 제한해두었기에 파국으로 치닫지는 않는다.

 

마찬가지로 도큐먼트에 보면 Connection option에 아래와 같은 옵션이 있다.

multipleStatements: Allow multiple mysql statements per query. Be careful with this, it could increase the scope of SQL injection attacks. (Default: false)

한 쿼리에서 여러 mysql 구문을 실행할 수 있도록 허용한다. SQL 주입 공격 유효범위가 늘어날 수 있음에 유의. (기본값은 false)

 

multipleStatements가 true인 경우에는 테이블이 DROP 될 수 있으니 조심하고, 기본값은 이유 없이 변경하지 말자. 모듈 사용 전에 도큐멘트만 잘 살펴봐도 대부분의 파국은 막을 수 있을 듯하다.

 

 

 

 

Cross site scripting (XSS) 예방법

기본에 다루었던 sanitize-html와 같은 방법으로 예방한다.

유저 입력 데이터를 처리하는 부분을 sanitizeHTML()로 감싸주면 된다.

유저 입력 데이터를 다루는 모든 부분에 처리가 필요하기 때문에 종종 빠뜨리는 부분이 나올 수 있다. 코딩할 때부터 미리미리 해두자.