소개
사용자가 제공한 이름은 값으로 유지되어야 하며 데이터베이스 명령의 일부가 되어서는 안 됩니다. 이 DeskLite 조회는 현재 이름을 사용하여 SQL 텍스트를 직접 만듭니다. 일반적인 요청은 동작하는 것처럼 보이지만 따옴표가 포함된 이름과 특수한 형태의 입력에서 문제가 드러납니다.
다섯 줄짜리 Python 함수만 읽고, 이 함수가 사용하는 소량의 SQL을 배우며, 실제 Semgrep 탐지 결과를 따라가고, SQLite 매개변수 바인딩 패턴 하나를 적용합니다. 사전 Python 또는 SQL 지식은 필요하지 않습니다. 실행 도구, 데이터베이스, 검사기, 기능 검사가 이미 준비되어 있습니다.
일반적인 조회 실행하기
이 단계에서는 구현을 읽기 전에 조회가 무엇을 반환해야 하는지 확인합니다.
DeskLite는 작은 SQLite 데이터베이스에 가상의 티켓 세 개를 저장합니다. SQLite는 로컬 파일을 읽고 조회하는 데이터베이스 엔진입니다. 교육 과정에서 연결과 데이터를 이미 준비했습니다.
cd /home/labex/project/ticket-queries
run_lookup.py는 준비된 진입점입니다. 의존성 실습과 마찬가지로 python3는 제공된 프로그램을 실행합니다. 마지막 인수는 조회할 이름입니다.
python3 run_lookup.py Alice
ID가 1이고 제목이 Printer offline인 티켓 하나가 예상됩니다. 실행 도구는 데이터베이스 행을 익숙한 JSON 응답 형식으로 만듭니다. 실행 도구의 서식 코드가 아니라 작은 조회 함수를 편집하게 됩니다.
의도한 경계는 간단합니다. 제공된 이름으로 해당 소유자의 티켓을 선택합니다. 데이터베이스 명령의 의미를 바꾸거나 다른 소유자의 티켓을 노출해서는 안 됩니다. 이 연습의 모든 이름과 기록은 가상입니다. 준비된 로컬 데이터베이스에서만 작업하세요.
다섯 줄 함수 읽기
이 단계에서는 작은 Python 함수에서 제공된 이름의 흐름을 따라갑니다.
cat lookup.py
파일에는 다음 내용이 있습니다.
from fixture import db
def lookup(name):
query = f"SELECT id, title FROM tickets WHERE owner = '{name}'"
return db.execute(query).fetchall()
from fixture import db는 준비된 데이터베이스 연결을 db라는 이름으로 사용할 수 있게 합니다. 함수는 이름이 붙은 재사용 가능한 동작 묶음입니다. def lookup(name):은 입력 이름이 name인 함수를 정의합니다. 들여쓴 줄은 해당 함수에 속하므로 편집할 때 들여쓰기를 유지하세요.
변수는 값에 이름을 붙입니다. query = ...는 문자열이라는 텍스트 값을 query에 대입합니다. 맨 앞의 f는 이를 f-string으로 만듭니다. Python은 {name}에 현재 name 값을 삽입합니다. 입력이 Alice이면 해당 텍스트가 SQL 문자열의 일부가 됩니다.
db.execute(query)는 연결에 쿼리 실행을 요청하는 함수와 비슷한 메서드 호출입니다. .fetchall()은 결과 행을 모읍니다. return은 해당 행들을 호출한 쪽으로 돌려줍니다. 준비된 실행 도구는 터미널 인수를 lookup에 전달하고 반환된 행을 표시합니다.
제공된 이름 → 쿼리 문자열 → 데이터베이스 호출 → 반환된 행이라는 데이터 경로만 이해하면 됩니다. 다음으로 해당 SQL 문자열의 의미를 읽어 봅니다.
의도된 SQL 조건 읽기
이 단계에서는 SQL 조건을 반환되어야 하는 기록과 연결합니다.
SQL은 데이터베이스 작업을 요청하는 언어입니다. 이 쿼리는 그중 작은 부분만 사용합니다.
SELECT id, title FROM tickets WHERE owner = 'Alice'
SELECT id, title은 결과 필드 두 개를 선택합니다. FROM tickets는 준비된 테이블을 선택합니다. WHERE owner = 'Alice'는 소유자가 해당 텍스트 값과 같은 행으로 결과를 제한합니다. SQL에서 작은따옴표는 텍스트 리터럴을 표시합니다. 이 SQL 문자열 안의 =는 소유자 조건을 나타냅니다. 문자열 밖의 Python 대입문 query = ...는 완성된 텍스트에 변수 이름을 붙입니다.
다른 일반적인 이름을 시도하세요.
python3 run_lookup.py Bob
제목이 Spare keyboard인 티켓 ID 3만 예상됩니다. 이름은 선택하는 열이나 테이블이 아니라 소유자 조건의 의도된 값을 바꿉니다.
이제 읽었던 f-string과 연결해 보세요. Python은 SQLite가 쿼리를 보기 전에 제공된 이름을 SQL 따옴표 사이에 삽입합니다. 이는 명령 구조와 입력 데이터라는 서로 다른 두 대상을 섞는 것입니다. 실제 이름 안의 따옴표는 이 경계를 흐트러뜨릴 수 있습니다.
아직 편집하지 마세요. 먼저 고정 검사기로 위험한 구성 방식을 찾은 다음, 따옴표가 포함된 유효한 이름과 허가된 로컬 테스트 입력으로 동작을 확인합니다.
정적 분석 탐지 결과 따라가기
이 단계에서는 실제 Semgrep 탐지 결과를 읽고 해당 위치를 쿼리 구성 방식과 연결합니다.
**정적 애플리케이션 보안 테스트(SAST)**는 가능한 모든 입력을 실행하지 않고 코드를 검사합니다. 준비된 실행 도구 course-code-scan은 버전이 고정된 Semgrep CE 1.145.0 엔진과 고정 로컬 규칙을 사용합니다. 클라우드 계정이나 온라인 규칙 모음은 사용하지 않습니다.
course-code-scan lookup.py
lookup.py:5에서 규칙 ID desklite.sql-parameter-binding과 다음 설명이 포함된 탐지 결과가 예상됩니다.
Bind the supplied lookup name as a SQL parameter instead of building SQL text from it.
해당 줄은 데이터베이스 호출에서 query를 사용하는 위치를 가리킵니다. 그 값을 한 줄 위로 따라가면 f-string을 만든 위치를 볼 수 있습니다. 이는 짧은 데이터 흐름 조사이며 전체 애플리케이션을 개발하라는 요청이 아닙니다.
탐지 결과가 있으면 1로 종료합니다. 파일을 분석할 수 없을 때는 CHECK ERROR와 함께 2로 종료합니다. 어느 쪽도 문제가 없다는 결과가 아닙니다. 로컬 규칙은 이 교육 과정에서 제공된 name이 SQL 텍스트로 들어가는 흐름을 추적합니다. 범위는 제한적입니다. 탐지 결과는 여전히 코드 검토가 필요하며, 규칙 하나에서 탐지되지 않았다고 애플리케이션에 다른 위험이 없다고 입증되지는 않습니다.
다음으로 탐지 결과를 실제 조회 동작과 비교합니다.
따옴표가 포함된 텍스트와 쿼리를 바꾸는 입력 비교하기
이 단계에서는 준비된 로컬 테스트 데이터만 사용하여 입력이 데이터로 유지되어야 하는 이유를 관찰합니다.
유효한 소유자 중 O'Neil이라는 사람이 있습니다. 큰따옴표는 아포스트로피를 포함한 터미널 인수 전체를 하나로 묶습니다.
python3 run_lookup.py "O'Neil"
Lookup failed:로 시작하며 SQLite 구문 오류가 포함된 줄이 예상됩니다. 이 실패는 결함을 보여주는 의도된 증거입니다. 프로그램이 이름을 명령 텍스트에 직접 삽입했기 때문에 이름의 아포스트로피가 SQL 텍스트 리터럴을 끝냈습니다. 모든 아포스트로피를 거부하면 실제 소유자의 조회가 깨집니다.
이제 범위가 한정된 다음 테스트 입력을 시도하세요.
python3 run_lookup.py "' OR 1=1 --"
안전하지 않은 버전은 일치하는 소유자가 없다는 결과 대신 티켓 세 개를 모두 반환합니다. 이 SQL 문맥에서 따옴표는 리터럴을 닫고, OR 1=1은 조건을 참으로 만들며, --는 나머지 텍스트를 주석 처리합니다. 입력이 이름으로 유지되지 않고 명령 구조를 바꾼 것입니다. 이를 SQL 인젝션이라고 합니다.
외부 서비스를 테스트하거나 데이터를 변경하는 것이 아닙니다. 준비된 데이터베이스는 읽기 전용이며 가상의 기록이 들어 있습니다. 필요한 수정은 두 관찰을 모두 충족해야 합니다. 따옴표가 포함된 유효한 이름은 동작해야 하고 쿼리를 바꾸려는 텍스트는 반환되는 행의 범위를 넓힐 수 없어야 합니다.
이름을 SQL 텍스트와 별도로 바인딩하기
이 단계에서는 SQLite의 매개변수 바인딩 인터페이스로 조회를 수정합니다.
자리표시자는 데이터 값이 들어갈 위치를 표시합니다. SQLite의 ? 자리표시자는 고정 SQL 구조의 일부이며 이름은 execute에 별도로 전달합니다. 데이터베이스 드라이버는 따옴표나 SQL처럼 보이는 문자가 포함되어 있어도 제공된 값을 데이터로 취급합니다.
nano lookup.py
가져오기 구문, 함수 이름, 들여쓰기를 유지하면서 함수의 쿼리 줄과 execute 줄을 다음의 지원되는 패턴으로 바꾸세요. Ctrl+O, Enter, Ctrl+X로 저장하세요.
from fixture import db
def lookup(name):
query = "SELECT id, title FROM tickets WHERE owner = ?"
return db.execute(query, (name,)).fetchall()
? 주위에 SQL 따옴표를 넣지 마세요. 값의 표현은 드라이버가 처리합니다. (name,)은 값들이 순서대로 들어 있는 모음인 튜플이며, 여기에는 원소가 하나 있습니다. 끝의 쉼표가 튜플을 만듭니다. 값 하나가 자리표시자 하나에 대응합니다. execute의 두 번째 인수는 이 튜플을 쿼리 텍스트와 별도로 전달합니다.
일반적인 조회를 실행하세요.
python3 run_lookup.py Alice
그런 다음 따옴표가 포함된 유효한 이름을 다시 시도하세요.
python3 run_lookup.py "O'Neil"
Alice에는 티켓 ID 1, O'Neil에는 제목이 Login issue인 티켓 ID 2가 예상됩니다. 둘 다 같은 고정 SQL 구조를 사용합니다. Python이 들여쓰기나 구문 오류를 보고하면 위의 짧은 함수를 복원하세요. 데이터베이스나 실행 도구를 수정하지 마세요.
거부와 유효한 동작 모두 확인하기
이 단계에서는 수정 후 유효한 이름이 유지되며 제공된 텍스트가 조회 범위를 넓히지 못하는지 확인합니다.
쿼리를 바꾸려는 입력을 반복하세요.
python3 run_lookup.py "' OR 1=1 --"
이제 빈 tickets 목록이 예상됩니다. SQLite는 그 텍스트 자체를 이름으로 가진 소유자를 찾으며 그런 소유자는 없습니다. 쿼리 구조는 추가되지 않았습니다.
일반적인 존재하지 않는 소유자를 확인하세요.
python3 run_lookup.py "Missing owner"
또 다른 빈 목록이 예상됩니다. 값이 없는 것과 애플리케이션이 고장 난 것은 다릅니다. 일반적인 이름과 따옴표가 포함된 유효한 이름은 여전히 각자의 티켓을 반환합니다.
의존성 실습과 같은 실행 형식으로 제공된 기능 회귀 검사를 실행하세요.
bash test.sh
예상 출력은 다음과 같습니다.
PASS: ordinary, quoted, missing, and injection-like names.
이 검사들은 실제 SQLite 호출과 결과 행을 테스트합니다. 함수를 제거하거나 모든 입력에 빈 목록을 반환하거나 데이터를 삭제해서는 유효한 결과를 유지할 수 없습니다. 실제 소유자의 이름에 아포스트로피가 있으므로 문자 블랙리스트도 충분하지 않습니다. 의도된 방어는 고정 SQL 텍스트와 바인딩된 값을 분리하는 것입니다.
수정한 소스 다시 검사하기
이 단계에서는 정적 분석 결과와 방금 테스트한 실제 조회 동작을 함께 확인합니다.
course-code-scan lookup.py
No findings under the fixed local rule.이 예상됩니다. 제공된 이름은 더 이상 SQL 명령 문자열로 들어가지 않고 별도로 바인딩된 값으로 전달됩니다.
기능 검사를 다시 실행하세요.
bash test.sh
같은 통과 결과가 예상됩니다. 검사와 동작 테스트는 서로 다른 질문에 답합니다. 하나는 대상 코드 패턴을 검사하고, 다른 하나는 실제 이름이 계속 동작하며 특수 입력이 결과 범위를 넓히지 못하는지 확인합니다.
교육 과정의 검증은 실제 SQLite 인수도 관찰합니다. 따라서 검사기의 탐지 결과만 숨기거나 따옴표 문자를 수동으로 바꾸는 것은 필요한 수정이 아닙니다. 원본 데이터베이스, 테스트 데이터 구성, 실행 도구, 함수 인터페이스를 유지하세요. 조사 파일을 작성할 필요는 없습니다.
이 실습에서는 작은 SQLite 바인딩 패턴 하나와 로컬 SAST 규칙 하나를 배웠습니다. 완전한 애플리케이션 검토에서는 다른 입력 출처와 취약점 유형도 고려해야 합니다. 여기서는 의도된 조회를 유지하면서 특정한 입력에서 쿼리로의 위험을 제거했습니다.
요약
짧은 Python 조회와 SQL 조건에서 제공된 이름의 흐름을 추적하고, 실제 로컬 Semgrep 탐지 결과를 실행 시 동작과 연결했으며, SQLite 매개변수 바인딩을 사용했습니다. 일반적인 이름과 따옴표가 포함된 이름은 계속 동작하고 쿼리를 바꾸려는 입력은 결과 범위를 넓힐 수 없습니다. 정적 분석과 기능 검사의 증거가 수정한 특정 경계를 뒷받침합니다.


