상품 검색 쿼리 수정

SQLiteBeginner
지금 연습하기

소개

작은 아웃도어 상점의 검색은 이름이 정확히 일치하는 등록 상품만 반환해야 합니다. 쿼리 구성 오류 때문에 입력이 이 규칙을 바꿀 수 있고 일반적인 아포스트로피도 검색을 망가뜨립니다. 실제 요청 경로를 관찰하고 짧은 Python 함수를 수정한 뒤 보안과 정상 결과를 시험합니다.

Workstation, App Server, Shop Web이 준비되어 있습니다. 기본 터미널 사용과 탭 전환만 알면 됩니다. SQL, 이번 수정에 필요한 Python 문법과 편집기 조작은 여기서 설명합니다. 데이터는 합성이지만 HTTP 서비스와 SQLite 구문 분석은 실제로 동작합니다.

일반 상품 검색 실행하기

이 단계에서는 상점을 사용하고 검색 결과를 데이터베이스 행과 연결합니다. 작은 아웃도어 상점의 검색을 고치는 상황입니다. 검색은 입력과 이름이 정확히 일치하는 등록 상품만 반환해야 합니다. 상품과 가격은 모두 합성 훈련 데이터입니다.

이 단계를 시작하려면 실습 인터페이스에서 Shop Web 탭을 선택합니다.

테이블은 레코드를 행으로 저장하며 이름이 붙은 열로 각 레코드를 설명합니다. 아래는 준비된 products 테이블입니다. listed가 1이면 공개 상품, 0이면 내부 시제품입니다. 가격은 훈련용 통화의 정수 단위입니다.

id name price listed
1 Trail Mug 18 1
2 Camper's Cup 22 1
3 Prototype Mug 99 0
4 ORANGE Bowl 12 1

Shop Web 탭으로 전환합니다. Product name에 Trail Mug를 입력하고 Search를 선택합니다. rows 결과에는 [1, "Trail Mug", 18]이 있고 error는 null(오류 없음)이어야 합니다. 페이지는 Python 앱에 요청을 보내고 앱은 SQLite에서 일치하는 레코드를 찾아 결과를 반환합니다. 페이지, 앱 코드와 데이터베이스는 이 요청 경로의 서로 다른 부분입니다.

정상 상품 결과와 학습용 SQL 추적을 보여 주는 Shop Web

쿼리는 데이터베이스에 하는 질문입니다. 로컬 학습용 추적에는 SQLite가 실행한 SQL이 표시됩니다.

SELECT id, name, price FROM products WHERE listed = 1 AND name = 'Trail Mug' ORDER BY id

SELECT는 반환할 열, FROM은 테이블을 지정합니다. WHERE는 행을 제한합니다. AND는 등록 여부와 정확한 이름이라는 두 조건을 모두 요구합니다. 작은따옴표는 SQL 텍스트 값을 구분합니다. ORDER BY id는 안정적인 행 순서를 제공합니다. 페이지는 listed 열을 반환하지 않지만 쿼리는 이를 필터에 사용합니다.

Workstation 터미널은 요청을 보내는 클라이언트입니다. App Server는 나중에 코드를 고칠 앱 디렉터리를 엽니다. 같은 훈련 호스트의 독립 세션이지 별도 컴퓨터가 아닙니다. 실제 앱이 제공하는 웹페이지에도 이 관계가 나옵니다.

Workstation 탭으로 전환하여 같은 요청을 보냅니다.

다음 명령을 실행하기 전에 Workstation 탭으로 전환합니다.

python3 probe.py "Trail Mug"

python3는 준비된 probe.py 클라이언트를 실행합니다. 인수 하나는 검색 텍스트입니다. 큰따옴표는 공백이 있는 이름을 한 인수로 유지하고 셸은 바깥 따옴표를 제거합니다. 클라이언트는 값을 안전하게 인코딩해 웹 앱의 요청 형식인 HTTP 요청으로 보냅니다. 클라이언트의 127.0.0.1은 이 훈련 호스트를 뜻합니다.

JSON 출력의 rows, trace, error를 읽습니다. JSON은 구조화된 응답이며 배열은 대괄호를 사용하고 null은 오류 없음을 나타냅니다. 페이지 연결이 안 되면 Shop Web을 새로 고칩니다. 연결 오류는 성공했지만 결과가 빈 검색과 다릅니다. 완료 검사는 실제 요청 결과를 읽으므로 보고서는 필요 없습니다.

입력이 쿼리를 바꾸는 과정 보기

이 단계에서는 훈련 상점 안에서 범위를 제한한 SQL 인젝션 테스트 하나를 관찰합니다. SQL 인젝션은 입력이 텍스트 값으로 남지 않고 쿼리 구조를 바꾸는 현상입니다. 이 테스트는 준비된 실습 안에서만 사용하세요.

이 단계를 시작하려면 실습 인터페이스에서 Workstation 탭을 선택합니다.

App Server 탭으로 전환합니다. 파일을 수정하지 않고 출력하는 cat으로 짧은 함수를 읽습니다.

다음 명령을 실행하기 전에 App Server 탭으로 전환합니다.

cat query.py

def search(db, text):는 함수를 정의합니다. db는 열린 데이터베이스 연결, text는 요청으로 받은 값입니다. 들여쓴 행이 함수 본문입니다. sql =는 변수에 Python 문자열을 대입합니다. 큰따옴표는 Python 문자열을, 그 안의 작은따옴표는 SQL 텍스트를 구분합니다. + text +는 사용자 값을 SQL에 직접 이어 붙입니다. db.execute(sql)은 SQLite에 실행을 요청하고, fetchall()은 결과 행을 모으며, return은 이를 반환합니다.

Workstation 탭으로 전환하고 두 하이픈 뒤의 공백까지 포함하여 다음 값을 정확히 전송합니다.

다음 명령을 실행하기 전에 Workstation 탭으로 전환합니다.

python3 probe.py "' OR 1=1 -- "

바깥 큰따옴표는 셸용입니다. 첫 작은따옴표가 SQLite에 들어가 이어 붙인 쿼리의 빈 텍스트 값을 닫습니다. OR는 어느 한 조건만 참이어도 허용하며 1=1은 항상 참입니다. --는 주석을 시작하므로 원래의 닫는 따옴표와 정렬 절은 무시됩니다. 추적에는 다음이 나옵니다.

SELECT id, name, price FROM products WHERE listed = 1 AND name = '' OR 1=1 -- ' ORDER BY id

[3, "Prototype Mug", 99]를 포함한 네 행 모두가 보입니다. 이 시제품은 미등록 상품입니다. 읽기만 했어도 요청이 의도한 업무 경계를 넘었습니다. HTTP 응답 성공만으로 안전한 검색이 입증되지는 않습니다.

아포스트로피가 있는 정상 이름도 시험합니다.

python3 probe.py "Camper's Cup"

취약한 앱은 rows: [], 빈 trace, near "s": syntax error가 포함된 error를 반환합니다. SQLite가 잘못된 SQL을 실행하지 못해 완료된 쿼리 추적도 없습니다. 예상된 실패이며 클라이언트는 정상 종료하므로 계속할 수 있습니다.

아포스트로피는 올바른 상품 데이터입니다. 삭제하면 요청한 이름이 달라지고 이를 포함한 입력을 전부 막으면 고객을 거부하게 됩니다. OR 같은 단어를 차단하면 ORANGE Bowl도 망가집니다. 값은 보존하고 SQL 구조와 분리해야 합니다.

검색 값 바인딩하기

이 단계에서는 열, 정확한 일치 조건, 게시 제한과 정렬 순서를 유지하면서 함수를 수정합니다. App Server 탭으로 전환합니다.

이 단계를 시작하려면 실습 인터페이스에서 App Server 탭을 선택합니다.

매개변수화 쿼리는 SQL 구조와 값을 분리합니다. Python SQLite API의 ?는 값 하나의 위치입니다. 값을 execute에 별도로 전달하며 자리 표시자에 따옴표를 씌우거나 값을 SQL에 연결하지 않습니다. 예를 들어 db.execute("SELECT ?", (text,))는 텍스트를 데이터로 반환하고 SQL로 해석하지 않습니다.

(text,)는 원소 하나의 튜플, 즉 순서가 있는 매개변수 모음입니다. 끝 쉼표가 튜플을 만들며 (text)만 쓰면 괄호 안의 원래 값일 뿐입니다. execute(sql, (text,))의 바깥 괄호는 SQL 템플릿과 값 튜플이라는 두 인수로 함수를 호출합니다. 드라이버는 값을 자리 표시자에 바인딩합니다.

터미널 편집기로 파일을 엽니다.

다음 명령을 실행하기 전에 App Server 탭으로 전환합니다.

nano query.py

방향키로 이동하고 Backspace/Delete로 지우며 글자를 입력해 편집합니다. sql =와 return으로 시작하는 두 들여쓰기 행을 바꿔 함수가 정확히 다음과 같게 합니다.

def search(db, text):
    sql = "SELECT id, name, price FROM products WHERE listed = 1 AND name = ? ORDER BY id"
    return db.execute(sql, (text,)).fetchall()

본문 각 행 앞에 공백 네 개를 두고 def 앞에는 들여쓰지 않습니다. Python은 들여쓰기로 본문을 묶습니다. Ctrl+O로 저장하고 Enter로 query.py를 확인한 뒤 Ctrl+X로 나갑니다. 원래 주석은 남겨도 됩니다.

앱은 요청마다 query.py를 읽고 컴파일합니다. 저장하면 적용되며 재시작은 필요 없습니다. 저장하지 않은 버퍼는 영향을 주지 않습니다. 오타나 들여쓰기 오류는 다음 응답의 error 필드에 나옵니다. 같은 파일을 다시 열어 고치고 저장한 뒤 요청을 재전송합니다.

Workstation 탭으로 전환하고 두 값을 다시 전송합니다.

다음 명령을 실행하기 전에 Workstation 탭으로 전환합니다.

python3 probe.py "Camper's Cup"
python3 probe.py "' OR 1=1 -- "

실제 상품은 이제 [2, "Camper's Cup", 22]를 반환하고 특수 테스트는 rows: []를 반환합니다. 둘 다 error: null입니다. 이 데이터베이스에는 그 특수 이름과 정확히 일치하는 상품이 없습니다.

추적에는 Camper''s Cup이나 따옴표로 둘러싼 전체 특수 문자열이 보일 수 있습니다. SQLite 추적은 이스케이프된 값을 포함한 바인딩 후 확장 SQL이며 원래 템플릿이 아닙니다. 값이 보인다고 Python이 이어 붙였다는 뜻은 아닙니다. query.py의 템플릿과 별도 튜플을 확인하세요. 바인딩 대신 수동 이스케이프를 만들지 마세요.

업무 검색 기능 유지하기

이 단계에서는 보안과 정상 기능을 함께 검사합니다. 검색을 끄거나 빈 고정 응답을 반환하면 시제품은 숨길 수 있어도 고객이 상품을 찾을 수 없게 됩니다.

이 단계를 시작하려면 실습 인터페이스에서 Workstation 탭을 선택합니다.

Workstation 탭으로 전환합니다. 정상 이름을 다시 전송합니다.

다음 명령을 실행하기 전에 Workstation 탭으로 전환합니다.

python3 probe.py "Trail Mug"

여전히 [1, "Trail Mug", 18]이 나와야 합니다. OR를 포함하는 올바른 단어도 거부되지 않는지 확인합니다.

python3 probe.py "ORANGE Bowl"

예상 결과는 [4, "ORANGE Bowl", 12]입니다. 미등록 시제품을 정상 이름으로 검색합니다.

python3 probe.py "Prototype Mug"

rows: []와 error: null이 예상됩니다. 바인딩은 인젝션을 막고, 유지된 listed = 1 제한은 공개 상품을 계속 결정합니다. 수정 중 이 제한을 지우면 안전하지 않습니다.

Shop Web 탭으로 돌아가 Camper's Cup을 입력하고 Search를 선택합니다. 해당 행이 나타나는지 확인합니다. ' OR 1=1 --로 반복하여 오류 없이 빈 결과가 나오는지 확인합니다. 브라우저와 클라이언트는 같은 쿼리 함수를 사용합니다.

수정된 쿼리가 입력을 데이터로 처리함을 보여 주는 Shop Web

단계 완료 검사를 실행합니다. 읽기 전용 요청을 보내고 임시 메모리 데이터베이스의 추가 합성 행으로 여러분의 변경되지 않은 함수를 실행합니다. 실제 데이터베이스를 편집하지 않고 정확한 일치, 문장 부호, 등록 규칙, 낯선 이름을 시험합니다. SQL처럼 보이는 상품 이름도 그 이름이 실제로 존재하고 등록되어 있으면 일반 데이터로 검색할 수 있어야 합니다.

검사가 실패하면 App Server 탭으로 돌아가 query.py를 다시 열고 자리표시자, 튜플의 쉼표, 들여쓰기와 업무 제한 조건을 수정 예시와 비교합니다. 저장한 후 다시 테스트합니다. 데이터베이스, 클라이언트와 서버 프레임워크는 유지합니다. 레코드를 바꾸거나 고정된 답을 반환하는 것은 쿼리를 수정하는 것이 아닙니다.

요약

검색을 페이지에서 실제 SQLite 쿼리까지 추적하고 입력이 구조를 바꾸는 현상을 관찰한 뒤 자리 표시자와 값 하나의 튜플로 Python 호출을 고쳤습니다. 정상 이름, 아포스트로피, 제한된 테스트, 상품 등록 규칙 유지를 확인했습니다. 주문 조회 챌린지에서는 같은 분리를 다른 업무 대상에 적용합니다.