SQL 삽입
적용 대상: Microsoft Fabric의 SQL Server Azure SQL Database Azure SQL Managed Instance Azure Synapse Analytics Analytics Platform System(PDW) SQL 데이터베이스
SQL 삽입은 나중에 구문 분석 및 실행을 위해 SQL Server 데이터베이스 엔진 인스턴스에 전달되는 문자열에 악성 코드가 삽입되는 공격입니다. 데이터베이스 엔진은 구문상 유효한 쿼리가 수신되면 모두 실행하기 때문에 SQL 문을 생성하는 모든 프로시저에 삽입 취약성이 있는지 검토해야 합니다. 매개 변수화된 데이터인 경우에도 숙련된 공격자에 의해 조작될 가능성이 있습니다.
SQL 삽입 작동 방식
SQL 주입의 주요 형태는 SQL 명령과 연결되어 실행되는 사용자 입력 변수에 코드를 직접 삽입하는 것으로 구성됩니다. 간접적인 공격에서는 테이블에 스토리지할 문자열에 또는 메타데이터로 악의적인 코드를 주입합니다. 저장된 문자열이 동적 SQL 명령에 연결되면 악성 코드가 실행됩니다.
삽입 프로세스는 텍스트 문자열을 중간에 종료하고 새 명령을 추가하는 방식으로 작동합니다. 삽입된 명령에는 실행 전에 덧붙여진 추가 문자열이 있을 수 있으므로 공격자는 주입된 문자열을 주석 표시인 --
로 종료합니다. 이후 텍스트는 실행 시 무시됩니다.
다음 스크립트는 간단한 SQL 삽입을 보여줍니다. 스크립트는 하드 코딩된 문자열을 사용자가 입력한 문자열과 연결하여 SQL 쿼리를 빌드합니다.
var ShipCity;
ShipCity = Request.form ("ShipCity");
var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";
사용자에게 도시 이름을 입력하라는 메시지가 표시됩니다. Redmond
를 입력하면 스크립트로 조합된 쿼리는 다음 예와 유사합니다.
SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';
그러나 사용자가 다음 텍스트를 입력한다고 가정합니다.
Redmond';drop table OrdersTable--
이 경우 스크립트는 다음 쿼리를 조합합니다.
SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';drop table OrdersTable--'
세미콜론(;
)은 한 쿼리의 끝과 다른 쿼리의 시작을 나타냅니다. 이중 하이픈(--
)은 현재 줄의 나머지 부분이 주석이므로 무시해야 함을 나타냅니다. 수정된 코드가 구문적으로 올바르면 서버에서 실행됩니다. 데이터베이스 엔진은 이 문을 처리할 때 먼저 OrdersTable
가 ShipCity
인 Redmond
의 모든 레코드를 선택합니다. 그런 다음 데이터베이스 엔진은 OrdersTable
을 삭제합니다.
삽입된 SQL 코드가 구문상 올바른 경우 프로그래밍 방식으로는 훼손 여부를 찾아낼 수 없습니다. 따라서 모든 사용자 입력의 유효성을 검사하고 생성된 SQL 명령을 현재 사용 중인 서버에서 실행하는 코드를 신중하게 검토해야 합니다. 코딩 모범 사례는 이 문서의 다음 섹션에 설명되어 있습니다.
모든 입력에 대한 유효성 검사
형식, 길이, 서식 및 범위를 테스트하여 항상 사용자 입력의 유효성을 검사합니다. 악의적인 입력에 대한 예방 조치를 구현할 때 애플리케이션의 아키텍처 및 배포 시나리오를 고려하세요. 보안 환경에서 실행되도록 설계된 프로그램은 안전하지 않은 환경에 복사할 수 있습니다. 다음과 같은 제안 사항을 모범 사례로 고려해야 합니다.
애플리케이션에서 받은 데이터의 크기, 형식 또는 콘텐츠에 대해 가정하지 않습니다. 예를 들어 다음과 같은 사항을 평가해야 합니다.
잘못되거나 악의적인 사용자가 애플리케이션에 우편 번호가 있는 2GB 비디오 파일을 입력하면 애플리케이션이 어떻게 작동하나요?
DROP TABLE
문이 텍스트 필드에 포함된 경우 애플리케이션은 어떻게 작동하나요?
입력의 크기와 데이터 형식을 테스트하고 적절한 제한을 적용합니다. 이렇게 하면 의도적인 버퍼 오버런을 방지할 수 있습니다.
문자열 변수의 내용을 테스트하고 예상된 값만 허용합니다. 이진 데이터, 이스케이프 시퀀스 및 주석 문자를 포함하는 항목은 거부합니다. 그러면 스크립트 삽입을 방지하고 일부 악의적인 버퍼 오버런으로부터 보호할 수 있습니다.
XML 문서 작업에서는 입력한 스키마에 대해 모든 데이터의 유효성을 검사합니다.
사용자 입력에서 직접 Transact-SQL 문을 빌드하지 않습니다.
저장 프로시저를 사용하여 사용자 입력의 유효성을 검사합니다.
다중 계층 환경에서는 신뢰할 수 있는 영역으로 들어가는 모든 데이터의 유효성을 검사해야 합니다. 유효성 검사 프로세스를 통과하지 못한 데이터를 거부하고 이전 계층으로 오류를 반환해야 합니다.
여러 유효성 검사 계층을 구현합니다. 악의적인 사용자에 대한 예방 조치는 결정된 공격자에 대해 비효율적일 수 있습니다. 사용자 인터페이스와 신뢰 경계를 넘는 모든 후속 지점에서 입력의 유효성을 검사하는 것이 더 좋습니다.
예를 들어 클라이언트 쪽 애플리케이션에서 데이터 유효성 검사를 수행하면 간단한 스크립트 삽입을 방지할 수 있습니다. 그러나 다음 계층에서 해당 입력의 유효성이 이미 검사되었다고 가정하면 클라이언트를 우회할 수 있는 악의적인 사용자는 시스템에 제한 없이 액세스할 수 있습니다.
유효성 검사를 수행하지 않은 사용자 입력을 연결하지 않습니다. 문자열 연결은 스크립트 삽입이 발생하는 주요 진입점입니다.
파일 이름을 구성할 수 있는 필드에
AUX
,CLOCK$
,COM1
~COM8
,CON
,CONFIG$
,LPT1
~LPT8
,NUL
및PRN
문자열을 허용하지 마세요.
가능하면 다음 문자가 포함된 입력을 거부합니다.
입력 문자 | Transact-SQL의 의미 |
---|---|
; |
쿼리 구분 기호. |
' |
문자 데이터 문자열 구분 기호 |
-- |
한 줄 주석 구분 기호. -- 다음부터 해당 줄 끝까지의 텍스트는 서버에서 평가되지 않습니다. |
/*** ... ***/ |
주석 구분 기호. /* 및 */ 사이의 텍스트는 서버에서 처리되지 않습니다. |
xp_ |
xp_cmdshell 과 같은 카탈로그 확장 저장 프로시저의 이름 시작 부분에 사용됩니다. |
형식이 안전한 SQL 매개 변수 사용
데이터베이스 엔진의 Parameters
컬렉션은 형식 검사와 길이 유효성 검사를 제공합니다. Parameters
컬렉션을 사용할 경우 입력은 실행 코드가 아닌 리터럴 값으로 처리됩니다. Parameters
컬렉션 사용의 또 다른 이점은 형식 및 길이 검사를 강제할 수 있다는 것입니다. 범위를 벗어난 값은 예외를 트리거합니다. 다음 코드 조각은 Parameters
컬렉션 사용을 보여줍니다.
SqlDataAdapter myCommand = new SqlDataAdapter("AuthorLogin", conn);
myCommand.SelectCommand.CommandType = CommandType.StoredProcedure;
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
SqlDbType.VarChar, 11);
parm.Value = Login.Text;
이 예에서는 @au_id
매개 변수가 실행 코드 대신 리터럴 값으로 처리됩니다. 이 값의 형식과 길이를 검사합니다. @au_id
값이 지정된 형식 및 길이 제약 조건을 준수하지 않으면 예외가 발생합니다.
저장 프로시저에 매개 변수화된 입력 사용
필터링되지 않은 입력을 사용하는 경우 저장 프로시저는 SQL 삽입에 취약할 수 있습니다. 예를 들어 다음 코드는 취약합니다.
SqlDataAdapter myCommand =
new SqlDataAdapter("LoginStoredProcedure '" + Login.Text + "'", conn);
저장 프로시저를 사용할 경우 입력으로 매개 변수를 사용합니다.
동적 SQL과 함께 매개 변수 컬렉션 사용
저장 프로시저를 사용할 수 없는 경우에도 다음 코드 예와 같이 매개 변수를 사용할 수 있습니다.
SqlDataAdapter myCommand = new SqlDataAdapter(
"SELECT au_lname, au_fname FROM Authors WHERE au_id = @au_id", conn);
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
SqlDbType.VarChar, 11);
parm.Value = Login.Text;
입력 필터링
입력 필터링은 이스케이프 문자를 제거하여 SQL 삽입으로부터 보호하는 데 도움이 될 수도 있습니다. 그러나 문제를 일으킬 수 있는 문자 수가 너무 많기 때문에 필터링은 신뢰할 수 있는 방어 수단이 아닙니다. 다음 예제에서는 문자열 구분 기호를 검색합니다.
private string SafeSqlLiteral(string inputSQL)
{
return inputSQL.Replace("'", "''");
}
LIKE 절
LIKE
절을 사용할 경우에도 와일드카드 문자를 이스케이프 처리해야 합니다.
s = s.Replace("[", "[[]");
s = s.Replace("%", "[%]");
s = s.Replace("_", "[_]");
SQL 삽입 코드 검토
EXECUTE
, EXEC
또는 sp_executesql
을 호출하는 모든 코드를 검토해야 합니다. 다음과 유사한 쿼리를 사용하여 이러한 문을 포함하는 절차를 식별할 수 있습니다. 이 쿼리는 EXECUTE
또는 EXEC
단어 뒤에 오는 1, 2, 3, 4개의 공백을 검사합니다.
SELECT object_Name(id)
FROM syscomments
WHERE UPPER(TEXT) LIKE '%EXECUTE (%'
OR UPPER(TEXT) LIKE '%EXECUTE (%'
OR UPPER(TEXT) LIKE '%EXECUTE (%'
OR UPPER(TEXT) LIKE '%EXECUTE (%'
OR UPPER(TEXT) LIKE '%EXEC (%'
OR UPPER(TEXT) LIKE '%EXEC (%'
OR UPPER(TEXT) LIKE '%EXEC (%'
OR UPPER(TEXT) LIKE '%EXEC (%'
OR UPPER(TEXT) LIKE '%SP_EXECUTESQL%';
QUOTENAME() 및 REPLACE()로 매개변수 래핑
선택한 각 저장 프로시저에서 동적 Transact-SQL에 사용되는 모든 변수가 올바르게 처리되는지 확인합니다. 저장 프로시저의 입력 매개 변수에서 가져온 데이터 또는 테이블에서 읽어 온 데이터는 QUOTENAME()
또는 REPLACE()
로 래핑해야 합니다. QUOTENAME()
에 전달되는 @variable의 값은 sysname이며 최대 길이는 128자입니다.
@variable | 권장 래퍼 |
---|---|
보안 개체의 이름 | QUOTENAME(@variable) |
128자보다 작거나 같은 문자열 | QUOTENAME(@variable, '''') |
128자보다 큰 문자열 | REPLACE(@variable,'''', '''''') |
이 기술을 사용하면 SET
문을 다음과 같이 수정할 수 있습니다.
-- Before:
SET @temp = N'SELECT * FROM authors WHERE au_lname ='''
+ @au_lname + N'''';
-- After:
SET @temp = N'SELECT * FROM authors WHERE au_lname = '''
+ REPLACE(@au_lname, '''', '''''') + N'''';
데이터 잘림으로 활성화된 삽입
변수에 할당되는 모든 동적 Transact-SQL은 해당 변수에 할당된 버퍼보다 클 경우 잘립니다. 문 잘림을 수행할 수 있는 공격자는 예기치 않게 긴 문자열을 저장 프로시저에 전달하여 결과를 조작할 수 있습니다. 예를 들어, 다음 예제 저장 프로시저는 잘림으로 활성화되는 삽입에 취약합니다.
이 예제에는 최대 길이가 200자인 @command
버퍼가 있습니다. 'sa'
의 비밀번호를 설정하려면 총 154자(UPDATE
문 26자, WHERE
절 16자, 'sa'
4자, QUOTENAME(@loginname)
으로 묶인 따옴표 2자: 200 - 26 - 16 - 4 - 2 = 154)가 필요합니다. 그러나 @new
가 sysname으로 선언되었기 때문에 이 변수는 128자만 포함할 수 있습니다. @new
에 작은따옴표를 전달하면 이 문제를 해결할 수 있습니다.
CREATE PROCEDURE sp_MySetPassword
@loginname SYSNAME,
@old SYSNAME,
@new SYSNAME
AS
-- Declare variable.
DECLARE @command VARCHAR(200)
-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password=' + QUOTENAME(@new, '''')
+ ' WHERE username=' + QUOTENAME(@loginname, '''')
+ ' AND password=' + QUOTENAME(@old, '''')
-- Execute the command.
EXEC (@command);
GO
공격자는 128자 버퍼에 154자를 전달하여 이전 암호를 몰라도 sa
에 새 암호를 설정할 수 있습니다.
EXEC sp_MySetPassword 'sa',
'dummy',
'123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012'''''''''''''''''''''''''''''''''''''''''''''''''''
이러한 이유로 명령 변수에 대해 큰 버퍼를 사용하거나 EXECUTE
문 내에서 동적 Transact-SQL을 직접 실행해야 합니다.
QUOTENAME(@variable, '''') 및 REPLACE() 사용 시 잘림
QUOTENAME()
및 REPLACE()
에서 반환된 문자열은 할당된 공간을 초과하는 경우 자동으로 잘립니다. 다음 예에서 만든 저장 프로시저는 발생할 수 있는 동작을 보여 줍니다.
이 예에서는 @login
, @oldpassword
및 @newpassword
의 버퍼 크기가 128자에 불과하지만 QUOTENAME()
은 최대 258자를 반환할 수 있으므로 임시 변수에 저장된 데이터가 잘립니다. @new
가 128자를 포함하는 경우 @newpassword
는 123... n
이 될 수 있습니다. 여기서 n
은 127번째 문자입니다. QUOTENAME()
에서 반환된 문자열이 잘렸기 때문에 다음 문과 같이 보일 수 있습니다.
UPDATE Users SET password ='1234...[127] WHERE username=' -- other stuff here
CREATE PROCEDURE sp_MySetPassword
@loginname SYSNAME,
@old SYSNAME,
@new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);
SET @login = QUOTENAME(@loginname, '''');
SET @oldpassword = QUOTENAME(@old, '''');
SET @newpassword = QUOTENAME(@new, '''');
-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users set password = ' + @newpassword
+ ' WHERE username = ' + @login
+ ' AND password = ' + @oldpassword;
-- Execute the command.
EXEC (@command);
GO
따라서 다음 문은 모든 사용자의 암호를 이전 코드에서 전달된 값으로 설정합니다.
EXEC sp_MyProc '--', 'dummy', '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678'
REPLACE()
를 사용하는 경우 할당된 버퍼 공간을 초과하여 문자열 잘림을 수행할 수 있습니다. 다음 예에서 만든 저장 프로시저는 발생할 수 있는 동작을 보여 줍니다.
이 예에서는 @login
, @oldpassword
및 @newpassword
에 할당된 버퍼가 128자만 보유할 수 있지만 QUOTENAME()
은 최대 258자를 반환할 수 있으므로 데이터가 잘립니다. @new
가 128자를 포함하는 경우 @newpassword
는 '123...n'
이 될 수 있습니다. 여기서 n
은 127번째 문자입니다. QUOTENAME()
에서 반환된 문자열이 잘렸기 때문에 다음 문과 같이 보일 수 있습니다.
UPDATE Users SET password='1234...[127] WHERE username=' -- other stuff here
CREATE PROCEDURE sp_MySetPassword
@loginname SYSNAME,
@old SYSNAME,
@new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);
SET @login = REPLACE(@loginname, '''', '''''');
SET @oldpassword = REPLACE(@old, '''', '''''');
SET @newpassword = REPLACE(@new, '''', '''''');
-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password = '''
+ @newpassword + ''' WHERE username = '''
+ @login + ''' AND password = ''' + @oldpassword + '''';
-- Execute the command.
EXEC (@command);
GO
QUOTENAME()
과 마찬가지로 REPLACE()
에 의한 문자열 잘림은 모든 경우에 대해 충분히 큰 임시 변수를 선언함으로써 방지할 수 있습니다. 가능한 경우 동적 Transact-SQL 내부에서 QUOTENAME()
또는 REPLACE()
를 직접 호출해야 합니다. 그렇지 않으면 다음과 같이 필요한 버퍼 크기를 계산할 수 있습니다. @outbuffer = QUOTENAME(@input)
의 경우 @outbuffer
의 크기는 2 * (len(@input) + 1)
이어야 합니다. 이전 예제와 같이 REPLACE()
와 큰따옴표를 사용하는 경우 2 * len(@input)
버퍼이면 충분합니다.
다음 계산에서는 모든 사례를 다룹니다.
WHILE LEN(@find_string) > 0, required buffer size =
ROUND(LEN(@input) / LEN(@find_string), 0)
* LEN(@new_string) + (LEN(@input) % LEN(@find_string))
QUOTENAME(@variable, ']') 사용 시 잘림
데이터베이스 엔진 보안 개체의 이름이 QUOTENAME(@variable, ']')
형식을 사용하는 문에 전달되면 잘림이 발생할 수 있습니다. 다음 예제에서는 이 시나리오를 보여 줍니다.
이 예에서 @objectname
은 2 * 258 + 1자를 허용해야 합니다.
CREATE PROCEDURE sp_MyProc
@schemaname SYSNAME,
@tablename SYSNAME
AS
-- Declare a variable as sysname. The variable will be 128 characters.
DECLARE @objectname SYSNAME;
SET @objectname = QUOTENAME(@schemaname) + '.' + QUOTENAME(@tablename);
-- Do some operations.
GO
sysname 형식의 값을 연결할 때는 값당 최대 128자를 저장할 수 있을 만큼 큰 임시 변수를 사용해야 합니다. 가능한 경우 동적 Transact-SQL 내부에서 QUOTENAME()
을 직접 호출합니다. 그렇지 않으면 이전 섹션에서 설명한 대로 필요한 버퍼 크기를 계산할 수 있습니다.