방어 기술에 대하여 — SQLite의 보안

방어 기술에 대하여 — SQLite의 보안 (Defense Against The Dark Arts)

SQLite는 항상 입력을 검증해요 (SQLite Always Validates Its Inputs)

SQLite는 악의적으로 잘못된 SQL 입력이나 데이터베이스 파일이 주어져도 결코 크래시하거나 버퍼를 오버플로하거나 메모리를 누수하거나 다른 유해한 동작을 보여서는 안 돼요. SQLite는 항상 잘못된 입력을 감지해 오류를 일으켜야 하며, 크래시하거나 메모리를 손상시켜서는 안 돼요. SQL 입력이나 데이터베이스 파일로 인해 발생하는 어떤 오작동도 심각한 버그로 간주되며, SQLite 개발자에게 알려지면 신속히 처리돼요. SQLite는 이러한 종류의 오류에 저항하도록 하기 위해 광범위하게 퍼즈 테스트(fuzz-test)돼요.

그럼에도 버그는 발생해요. 신뢰할 수 없는 SQL 입력이나 데이터베이스 파일을 SQLite에 보내는 애플리케이션을 작성한다면, 공격 표면(attack surface)을 줄이고 감지되지 않은 버그로 인한 제로데이(zeroday) 익스플로잇을 방지하기 위해 취할 수 있는 추가 단계가 있어요.

출처: 문서

본문

1. 신뢰할 수 없는 SQL 입력 (Untrusted SQL Inputs)

신뢰할 수 없는 SQL 입력을 받아들이는 애플리케이션은 다음 예방 조치를 취해야 해요.

  1. SQLITE_DBCONFIG_DEFENSIVE 플래그를 설정하세요. 이는 일반 SQL 문이 의도적으로 데이터베이스 파일을 손상시키는 것을 방지해요. SQLite는 악의적인 SQL 입력과 악의적으로 손상된 데이터베이스 파일이 동시에 관련된 공격에도 견딜 수 있어야 해요. 그럼에도 스크립트 전용 공격자가 손상된 데이터베이스 입력에 접근하는 것을 차단하는 것은 추가 방어층을 제공해요.

  2. SQLite가 입력에 부과하는 제한을 줄이세요. 이는 서비스 거부(denial of service) 공격과 비정상적으로 큰 입력으로 인해 발생할 수 있는 다른 종류의 장난을 방지하는 데 도움이 돼요. 컴파일 타임에 -DSQLITE_MAX_... 옵션을 사용하거나, 런타임에 sqlite3_limit() 인터페이스를 사용해 할 수 있어요. 대부분의 애플리케이션은 기능에 영향을 주지 않으면서 제한을 크게 줄일 수 있어요. 아래 표는 몇 가지 제안을 제공해요. 정확한 값은 애플리케이션에 따라 달라져요.

| 제한 설정 | 기본값 | 고보안 값 | | LIMIT_LENGTH | 1,000,000,000 | 1,000,000 | | LIMIT_SQL_LENGTH | 1,000,000,000 | 100,000 | | LIMIT_COLUMN | 2,000 | 100 | | LIMIT_EXPR_DEPTH | 1,000 | 10 | | LIMIT_PARSER_DEPTH | 2,500 | 100 | | LIMIT_COMPOUND_SELECT | 500 | 3 | | LIMIT_VDBE_OP | 250,000,000 | 25,000 | | LIMIT_FUNCTION_ARG | 127 | 8 | | LIMIT_ATTACH | 10 | 0 | | LIMIT_LIKE_PATTERN_LENGTH | 50,000 | 50 | | LIMIT_VARIABLE_NUMBER | 999 | 10 | | LIMIT_TRIGGER_DEPTH | 1,000 | 10 |

큰 제한은 임의 SQL을 주입할 수 있는 공격자가 깊은 재귀와 그로 인한 CPU 스택 오버플로를 일으키는 쿼리를 구성할 수 있게 할 수 있어요. 기본 제한은 대부분의 머신에서 이를 방지하기에 충분하지만, 스택 공간이 제한된 프로세서에서 실행한다면 LIMIT_EXPR_DEPTH와 LIMIT_TRIGGER_DEPTH를 10 같은 작은 값으로 설정하도록 주의하세요.

  1. 처리할 SQL의 범위를 제한하기 위해 sqlite3_set_authorizer() 인터페이스 사용을 고려하세요. 예를 들어 데이터베이스 스키마를 변경할 필요가 없는 애플리케이션은 CREATE나 DROP 문을 실패시키는 sqlite3_set_authorizer() 콜백을 추가할 수 있어요.

  2. SQL 언어는 매우 강력하므로, 악의적인 SQL 입력(또는 애플리케이션 버그로 인한 잘못된 SQL 입력)이 매우 오래 실행되는 SQL을 제출하는 것이 항상 가능해요. 이것이 서비스 거부 공격이 되지 않도록 하려면 sqlite3_progress_handler() 인터페이스를 사용해 각 SQL 문이 실행될 때 주기적으로 콜백을 호출하고, 문이 너무 오래 실행되면 그 콜백이 0이 아닌 값을 반환해 문을 중단하게 하는 것을 고려하세요. 또는 별도의 스레드에서 타이머를 설정하고 타이머가 울리면 sqlite3_interrupt()를 호출해 SQL 문이 영원히 실행되지 않게 하세요.

  3. sqlite3_hard_heap_limit64() 인터페이스를 사용해 SQLite가 할당할 최대 메모리 양을 제한하세요. 이는 서비스 거부 공격을 방지하는 데 도움이 돼요. 애플리케이션이 실제로 필요한 힙 공간을 알아내려면 일반적인 입력으로 실행해 본 다음 sqlite3_memory_highwater() 인터페이스로 최대 순간 메모리 사용량을 측정하세요. 하드 힙 제한을 관찰된 최대 순간 메모리 사용량에 여유를 더한 값으로 설정하세요.

  4. SQLITE_MAX_ALLOCATION_SIZE 컴파일 타임 옵션을 기본값인 2147483391(0x7ffffeff)보다 작은 값으로 설정하는 것을 고려하세요. 애플리케이션에 따라 100000000(1억) 또는 그보다 작은 값도 합리적이에요.

  5. 임베디드 시스템의 경우 -DSQLITE_ENABLE_MEMSYS5 옵션으로 SQLite를 컴파일한 다음 sqlite3_config(SQLITE_CONFIG_HEAP) 인터페이스를 통해 SQLite에 힙으로 사용할 고정된 메모리 청크를 제공하는 것을 고려하세요. 이는 악의적인 SQL이 과도한 메모리를 사용해 서비스 거부 공격을 실행하는 것을 방지해요. 예를 들어 SQLite에 5MB의 메모리를 제공한다면, 그 양이 소비되고 나면 SQLite는 애플리케이션의 다른 부분이 필요로 하는 메모리를 흡수하는 대신 SQLITE_NOMEM 오류를 반환하기 시작할 거예요. 이는 또한 SQLite의 메모리를 샌드박스 처리해서 애플리케이션의 다른 부분에서 발생하는 use-after-free 오류가 SQLite에 문제를 일으키지 않게 해줘요. 그 반대도 마찬가지예요.

  6. printf() SQL 함수의 메모리 사용을 제어하려면 "-DSQLITE_PRINTF_PRECISION_LIMIT=100000" 또는 그와 비슷하게 합리적인 값으로 컴파일하세요. 이 #define은 printf() 함수에서 %-치환의 너비와 정밀도를 제한하며, 따라서 "printf('%1000000000s','hi')" 같은 구문을 통해 적대적인 SQL 문이 많은 양의 RAM을 소비하는 것을 방지해요.

SQLite는 sqlite_schema 테이블의 sql 열 형식을 지정하는 데 내부적으로 내장 printf()를 사용한다는 점에 주의하세요. 그 이유로 인해 어떤 테이블·인덱스·뷰·트리거 정의도 정밀도 제한보다 훨씬 클 수 없어요. 100000보다 작은 정밀도 제한을 설정할 수 있지만, 사용하는 정밀도 제한이 스키마에서 가장 긴 CREATE 문만큼은 길어야 하도록 주의하세요.

2. 신뢰할 수 없는 SQLite 데이터베이스 파일 (Untrusted SQLite Database Files)

출처가 불확실한 SQLite 데이터베이스 파일을 읽거나 쓰는 애플리케이션은 아래 나열된 예방 조치를 취해야 해요.

애플리케이션이 신뢰할 수 없는 출처의 데이터베이스 파일을 의도적으로 받아들이지 않더라도, 로컬 데이터베이스 파일이 변경되는 공격을 조심하세요. 최상의 보안을 위해, 다른 보안 도메인의 에이전트에 의해 쓰여졌을 가능성이 있는 데이터베이스 파일은 모두 의심스러운 것으로 취급해야 해요.

  1. 애플리케이션이 부작용이 있거나 권한 있는 정보를 누출할 수 있는 사용자 정의 SQL 함수사용자 정의 가상 테이블을 포함한다면, 악의적으로 제작된 데이터베이스 스키마가 그 SQL 함수 및/또는 가상 테이블을 은밀하게 악의적인 목적으로 실행하지 못하도록 아래 기법 중 하나 이상을 사용해야 해요.

  2. 데이터베이스 연결을 열자마자 sqlite3_db_config(db,SQLITE_DBCONFIG_TRUSTED_SCHEMA,0,0)을 호출하세요.

  3. 각 데이터베이스 연결을 열자마자 PRAGMA trusted_schema=OFF 문을 실행하세요.

  4. -DSQLITE_TRUSTED_SCHEMA=0 컴파일 타임 옵션으로 SQLite를 컴파일하세요.

  5. 모든 사용자 정의 SQL 함수에 SQLITE_DIRECTONLY 플래그를, 모든 사용자 정의 가상 테이블에 SQLITE_VTAB_DIRECTONLY 플래그를 설정해 사용자 정의 SQL 함수와 가상 테이블의 은밀한 사용을 비활성화하세요.

  6. 데이터베이스 스키마의 CREATE VIRTUAL TABLE 문 인수를 기반으로 SQL 문을 실행하는 사용자 정의 가상 테이블이 SQLITE_PREPARE_FROM_DDL 옵션으로 sqlite3_prepare_v3()를 사용해 위 항목 (1)의 SQLITE_DBCONFIG_TRUSTED_SCHEMA 설정 우회를 방지하도록 하세요.

  7. 애플리케이션이 트리거나 뷰를 사용하지 않는다면 사용하지 않는 기능을 다음과 같이 비활성화하는 것을 고려하세요.

sqlite3_db_config(db,SQLITE_DBCONFIG_ENABLE_TRIGGER,0,0);
sqlite3_db_config(db,SQLITE_DBCONFIG_ENABLE_VIEW,0,0);

애플리케이션이 특히 보안에 민감하다면 다음 추가 방어층이 정당화될 수 있어요. 하지만 이 추가 방어는 성능 비용을 수반하므로 모든 상황에서 적절하지 않을 수 있어요.

  1. 데이터베이스 파일을 연 후 다른 SQL 문을 실행하기 전에 첫 번째 SQL 문으로 PRAGMA integrity_check 또는 PRAGMA quick_check를 실행하세요. 오류를 포함한 데이터베이스 파일은 모두 거부하고 처리하지 마세요.

  2. PRAGMA cell_size_check=ON 설정을 활성화하세요.

  3. 메모리 매핑 I/O를 활성화하지 마세요. 즉 PRAGMA mmap_size=0인지 확인하세요.

요약 (Summary)

위 예방 조치는 잠재적으로 적대적인 입력과 함께 SQLite를 안전하게 사용하기 위해 필요한 것은 아니에요. 하지만 제로데이 익스플로잇에 대한 추가 방어층을 제공하며, 신뢰할 수 없는 출처의 데이터를 SQLite로 전달하는 애플리케이션에 권장돼요.

더 알아보기 (Learn more)