Introdução
Um nome fornecido por um usuário deve permanecer um valor, sem se tornar parte de um comando de banco de dados. Atualmente, esta busca do DeskLite constrói texto SQL diretamente a partir de um nome. Requisições comuns parecem funcionar, mas nomes com apóstrofos e entradas especialmente elaboradas revelam o problema.
Você lerá apenas uma função Python de cinco linhas, aprenderá a pequena quantidade de SQL que ela usa, acompanhará uma detecção real do Semgrep e aplicará um padrão de vinculação de parâmetros do SQLite. Não é necessário conhecimento prévio de Python ou SQL. O executor, o banco de dados, o scanner e a verificação funcional já estão preparados.
Execute uma busca comum
Nesta etapa, você verificará o que a busca deve retornar antes de ler sua implementação.
O DeskLite armazena três tickets fictícios em um pequeno banco de dados SQLite. SQLite é um mecanismo de banco de dados que lê e consulta um arquivo local. O curso já preparou a conexão e os dados.
cd /home/labex/project/ticket-queries
run_lookup.py é um ponto de entrada preparado. Como no laboratório de dependências, python3 executa um programa fornecido. O último argumento é o nome a buscar:
python3 run_lookup.py Alice
Espere um ticket com ID 1 e título Printer offline. O executor formata as linhas do banco de dados na resposta JSON já conhecida. Você editará a pequena função de busca, não o código de formatação do executor.
A restrição pretendida é simples: o nome fornecido seleciona os tickets daquele proprietário. Ele não deve alterar o significado do comando de banco de dados nem revelar tickets de outros proprietários. Todos os nomes e registros deste exercício são fictícios; trabalhe apenas com este banco de dados local preparado.
Leia a função de cinco linhas
Nesta etapa, você acompanhará o nome fornecido por uma pequena função Python.
cat lookup.py
O arquivo contém:
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 disponibiliza a conexão preparada com o banco de dados sob o nome db. Uma função é um trecho nomeado de comportamento reutilizável. def lookup(name): define uma função cuja entrada se chama name. As linhas indentadas pertencem a essa função; mantenha sua indentação ao editar.
Uma variável dá nome a um valor. query = ... atribui um valor de texto, chamado string, a query. O f inicial faz disso uma f-string: o Python insere o valor atual de name em {name}. Se a entrada for Alice, esse texto se torna parte da string SQL.
db.execute(query) é uma chamada de método, semelhante a uma chamada de função, que pede à conexão que execute a consulta. .fetchall() coleta suas linhas de resultado. return envia essas linhas de volta a quem chamou a função. O executor preparado passa seu argumento do terminal para lookup e exibe as linhas retornadas.
Você só precisa deste caminho dos dados: nome fornecido → string da consulta → chamada ao banco de dados → linhas retornadas. A seguir, você lerá o que essa string SQL significa.
Interprete a condição SQL pretendida
Nesta etapa, você relacionará a condição SQL aos registros que devem ser retornados.
SQL é uma linguagem para solicitar operações de banco de dados. Esta consulta usa apenas uma pequena parte dela:
SELECT id, title FROM tickets WHERE owner = 'Alice'
SELECT id, title escolhe os dois campos do resultado. FROM tickets escolhe a tabela preparada. WHERE owner = 'Alice' limita os resultados às linhas cujo proprietário é igual a esse valor de texto. Em SQL, as aspas simples delimitam um literal de texto. O = dentro desta string SQL expressa a condição do proprietário. A atribuição Python query = ... fora da string dá ao texto completo um nome de variável.
Tente outro nome comum:
python3 run_lookup.py Bob
Espere apenas o ticket com ID 3, intitulado Spare keyboard. O nome altera o valor pretendido da condição do proprietário, não as colunas selecionadas nem a tabela.
Agora relacione isso à f-string que você leu. O Python insere o nome fornecido entre as aspas do SQL antes de o SQLite receber a consulta. Isso combina duas coisas diferentes: a estrutura do comando e os dados de entrada. Uma aspa dentro de um nome real pode interferir nessa separação.
Ainda não edite. Primeiro, você usará o scanner fixo para localizar a construção arriscada e depois confirmará o comportamento com um nome legítimo contendo apóstrofo e uma entrada de teste local autorizada.
Acompanhe a detecção da análise estática
Nesta etapa, você lerá uma detecção real do Semgrep e relacionará sua localização à construção da consulta.
O teste estático de segurança de aplicativos (SAST) examina o código sem executar todas as entradas possíveis. O inicializador preparado course-code-scan usa o mecanismo Semgrep CE 1.145.0 com versão fixada e uma regra local fixa. Não há conta na nuvem nem conjunto de regras online.
course-code-scan lookup.py
Espere uma detecção em lookup.py:5, com o ID de regra desklite.sql-parameter-binding e esta explicação:
Bind the supplied lookup name as a SQL parameter instead of building SQL text from it.
A linha aponta para o uso de query na chamada ao banco de dados. Acompanhe esse valor uma linha acima para ver onde a f-string foi construída. Esta é uma breve investigação de fluxo de dados, não um pedido para desenvolver um aplicativo completo.
Uma detecção termina com 1; CHECK ERROR termina com 2 quando o arquivo não pode ser analisado. Nenhum dos dois é um resultado sem problemas. A regra local acompanha o name fornecido neste curso até o texto SQL. Seu escopo é limitado: uma detecção ainda precisa de revisão do código, e a ausência de detecção com uma única regra não prova que um aplicativo não tenha outros riscos.
A seguir, você comparará a detecção com o comportamento real da busca.
Compare texto com apóstrofo e entrada que altera a consulta
Nesta etapa, você observará por que a entrada deve permanecer como dado, usando apenas os dados locais de teste preparados.
Um proprietário legítimo se chama O'Neil. As aspas duplas mantêm esse argumento inteiro do terminal unido, incluindo seu apóstrofo:
python3 run_lookup.py "O'Neil"
Espere uma linha iniciada por Lookup failed: com um erro de sintaxe do SQLite. Essa falha é uma evidência intencional do defeito. O apóstrofo do nome encerrou um literal de texto SQL porque o programa o inseriu diretamente no texto do comando. Rejeitar todo apóstrofo impediria a busca de um proprietário real.
Agora tente esta entrada de teste de escopo limitado:
python3 run_lookup.py "' OR 1=1 --"
A versão insegura retorna os três tickets, em vez de indicar que nenhum proprietário corresponde. Neste contexto SQL, a aspa fecha o literal, OR 1=1 torna a condição verdadeira e -- transforma o texto restante em comentário. A entrada alterou a estrutura do comando em vez de permanecer um nome. Isso é injeção de SQL.
Você não está testando um serviço externo nem alterando dados. O banco de dados preparado é somente leitura e contém registros fictícios. A correção exigida deve atender às duas observações: um nome legítimo com apóstrofo funciona, enquanto um texto que altera a consulta não consegue ampliar as linhas retornadas.
Vincule o nome separadamente do texto SQL
Nesta etapa, você corrigirá a busca usando a interface de vinculação de parâmetros do SQLite.
Um marcador indica onde um valor de dado deve entrar. O marcador ? do SQLite faz parte da estrutura SQL fixa; o nome é fornecido separadamente a execute. O driver do banco de dados trata o valor fornecido como dado, mesmo que contenha aspas ou caracteres que pareçam SQL.
nano lookup.py
Substitua as linhas de consulta e execução da função por este padrão compatível, preservando a importação, o nome da função e a indentação. Salve com 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()
Não coloque aspas SQL ao redor de ?. O driver cuida da representação do valor. (name,) é uma tupla de um elemento, uma coleção ordenada de valores. A vírgula final faz com que seja uma tupla; seu único valor corresponde ao único marcador. O segundo argumento de execute transporta essa tupla separadamente do texto da consulta.
Execute uma busca comum:
python3 run_lookup.py Alice
Depois tente novamente o nome legítimo com apóstrofo:
python3 run_lookup.py "O'Neil"
Espere o ticket com ID 1 para Alice e o ticket com ID 2, intitulado Login issue, para O'Neil. Ambos usam a mesma estrutura SQL fixa. Se o Python informar um erro de indentação ou sintaxe, restaure a pequena função mostrada acima; não modifique o banco de dados nem o executor.
Verifique a rejeição e o comportamento legítimo
Nesta etapa, você confirmará que a correção preserva os nomes válidos e impede que o texto fornecido amplie a busca.
Repita a entrada que alterava a consulta:
python3 run_lookup.py "' OR 1=1 --"
Agora espere uma lista tickets vazia. O SQLite procura um proprietário cujo nome seja literalmente esse texto, e nenhum existe. Nenhuma estrutura de consulta foi adicionada.
Verifique um proprietário comum inexistente:
python3 run_lookup.py "Missing owner"
Espere outra lista vazia. Um valor inexistente é diferente de um aplicativo com defeito: nomes comuns e nomes legítimos com apóstrofos continuam retornando seus próprios tickets.
Execute a verificação de regressão funcional fornecida usando o mesmo estilo de entrada do laboratório de dependências:
bash test.sh
Espere:
PASS: ordinary, quoted, missing, and injection-like names.
Essas verificações exercitam chamadas reais ao SQLite e linhas de resultado. Remover a função, retornar uma lista vazia para toda entrada ou excluir dados não preservaria os resultados legítimos. Uma lista de bloqueio de caracteres também é insuficiente: um proprietário real tem um apóstrofo no nome. A defesa pretendida é a separação entre texto SQL fixo e valores vinculados.
Examine novamente o código-fonte corrigido
Nesta etapa, você combinará o resultado estático com o comportamento real da busca que acabou de testar.
course-code-scan lookup.py
Espere No findings under the fixed local rule. O nome fornecido não flui mais para a string do comando SQL. Ele é passado como um valor vinculado separado.
Execute novamente a verificação funcional:
bash test.sh
Espere o mesmo resultado de sucesso. A varredura e o teste de comportamento respondem a perguntas diferentes: uma examina o padrão de código específico; o outro verifica se os nomes reais continuam funcionando e se uma entrada especial não consegue ampliar o resultado.
A verificação do curso também observa os argumentos reais do SQLite, portanto simplesmente ocultar uma detecção do scanner ou substituir manualmente caracteres de aspas não é a correção exigida. Preserve o banco de dados, os dados de teste, o executor e a interface da função originais. Não é necessário criar um arquivo de investigação escrito.
Este laboratório ensinou um pequeno padrão de vinculação do SQLite e uma regra local de SAST. Uma revisão completa de aplicativo também consideraria outras fontes de entrada e classes de vulnerabilidades. Aqui, você removeu o risco específico de entrada incorporada à consulta, preservando a busca pretendida.
Resumo
Você acompanhou um nome fornecido por uma pequena função de busca Python e uma condição SQL, relacionou uma detecção real do Semgrep local ao comportamento em execução e usou a vinculação de parâmetros do SQLite. Nomes comuns e nomes com apóstrofos continuam funcionando, enquanto entradas que alteravam a consulta não conseguem ampliar os resultados. As evidências estáticas e funcionais sustentam a restrição específica corrigida.


