DBMS_ASSET: Um pacote para prevenir a injeção de SQL
A injeção de SQL é uma técnica de ataque que explora a codificação frouxa de aplicações de banco de dados. Ela consiste em inserir código SQL malicioso em campos de entrada de dados, com o objetivo de alterar, excluir ou extrair informações sensíveis do banco de dados. A injeção de SQL pode causar danos graves à segurança e à integridade dos dados, além de comprometer a disponibilidade e o desempenho do sistema.
Uma das formas de prevenir a injeção de SQL é usar variáveis de ligação (bind variables), que permitem separar o código SQL da entrada de dados, evitando que o atacante modifique a estrutura ou a lógica da consulta. No entanto, nem todas as aplicações usam variáveis de ligação, e algumas situações exigem a construção dinâmica de consultas SQL, o que aumenta o risco de injeção de SQL.
Para esses casos, o Oracle 19c oferece o pacote DBMS_ASSET, que fornece uma interface para validar propriedades do valor de entrada, como o nome de um objeto, um esquema, uma coluna ou uma expressão SQL. O pacote DBMS_ASSET contém várias funções que podem ser usadas para sanitizar a entrada de dados e ajudar a proteger contra a injeção de SQL em aplicações que não usam variáveis de ligação.
As funções do pacote DBMS_ASSET
O pacote DBMS_ASSET contém as seguintes funções:
- NOOP: Retorna o valor de entrada sem nenhuma verificação.
- SIMPLE_SQL_NAME: Verifica se o valor de entrada é um nome SQL simples, ou seja, um identificador que consiste em até 30 caracteres alfanuméricos, sublinhados (_), cifrões ($) ou cerquilhas (#), e que não contém espaços ou caracteres especiais. Se o valor de entrada não for um nome SQL simples, a função gera um erro de valor.
- QUALIFIED_SQL_NAME: Verifica se o valor de entrada é um nome SQL qualificado, ou seja, um identificador que consiste em até quatro nomes SQL simples separados por pontos (.), representando o nome de um objeto, um esquema, um banco de dados ou um domínio. Se o valor de entrada não for um nome SQL qualificado, a função gera um erro de valor.
- SCHEMA_NAME: Verifica se o valor de entrada é um nome de esquema existente no banco de dados. Se o valor de entrada não for um nome de esquema válido, a função gera um erro de valor.
- SQL_OBJECT_NAME: Verifica se o valor de entrada é um nome SQL qualificado de um objeto SQL existente no banco de dados, como uma tabela, uma visão, uma sequência, um índice, etc. Se o valor de entrada não for um nome de objeto SQL válido, a função gera um erro de valor.
- ENQUOTE_NAME: Garante que o valor de entrada esteja entre aspas duplas, e verifica se o resultado é um nome SQL simples válido. Se o valor de entrada já estiver entre aspas, a função não adiciona aspas adicionais. Se o valor de entrada não for um nome SQL simples válido, a função gera um erro de valor.
- ENQUOTE_LITERAL: Adiciona aspas simples no início e no final do valor de entrada, formando um literal de texto. Se o valor de entrada já estiver entre aspas simples, a função não adiciona aspas adicionais. Se o valor de entrada contiver aspas simples internas, a função as duplica para escapá-las.
Exemplos de uso do pacote DBMS_ASSET
A seguir, apresentarei alguns exemplos de como usar as funções do pacote DBMS_ASSET para prevenir a injeção de SQL em aplicações que constroem consultas dinamicamente.
Exemplo 1: Criar uma tabela com o nome fornecido pelo usuário
Suponha que você tenha uma aplicação que permite ao usuário criar uma tabela com um nome de sua escolha. Você poderia usar a função SIMPLE_SQL_NAME para validar o nome da tabela antes de executar a consulta SQL, evitando que o usuário insira um nome inválido ou malicioso. Veja o exemplo abaixo:
-- Este procedimento cria uma tabela com uma única coluna no esquema do criador do procedimento.
create or replace procedure createOneColumnTable (proposedTableName varchar2) is
v_tableName varchar2(30);
begin
if (proposedTableName is null) then
raise value_error;
end if;
-- O uso de SIMPLE_SQL_NAME garante que o nome da tabela seja um identificador simples e válido.
v_tableName := sys.DBMS_ASSERT.SIMPLE_SQL_NAME(proposedTableName);
-- A consulta SQL é construída dinamicamente usando o nome da tabela validado.
execute immediate 'create table ' || v_tableName || ' (col1 number)';
end;
/
Resultado
- Se o usuário fornecer um nome válido, como
myTable
, o procedimento irá criar uma tabela com esse nome e uma coluna chamadacol1
do tiponumber
. - Se o usuário fornecer um nome inválido, como
my Table
oudrop table users
, o procedimento irá gerar um erro de valor e não irá executar a consulta SQL.
SQL> set serveroutput on
SQL> exec createOneColumnTable('myTable');
PL/SQL procedure successfully completed.
SQL> desc myTable;
Name Null? Type
----------------------------------------- -------- ----------------------------
COL1 NUMBER
SQL> exec createOneColumnTable('my Table');
BEGIN createOneColumnTable('my Table'); END;
*
ERROR at line 1:
ORA-44003: invalid SQL name
ORA-06512: at "SYS.DBMS_ASSERT", line 316
ORA-06512: at "SCOTT.CREATEONECOLUMNTABLE", line 9
ORA-06512: at line 1
Exemplo 2: Consultar uma tabela com o nome fornecido pelo usuário
Suponha que você tenha uma aplicação que permite ao usuário consultar uma tabela com um nome de sua escolha. Você poderia usar a função SQL_OBJECT_NAME para validar o nome da tabela antes de executar a consulta SQL, evitando que o usuário insira um nome inválido ou malicioso. Veja o exemplo abaixo:
-- Esta função retorna o número de linhas de uma tabela com o nome fornecido pelo usuário.
create or replace function countRows (proposedTableName varchar2) return number is
v_tableName varchar2(128);
v_count number;
begin
if (proposedTableName is null) then
raise value_error;
end if;
-- O uso de SQL_OBJECT_NAME garante que o nome da tabela seja um identificador qualificado e existente.
v_tableName := sys.DBMS_ASSERT.SQL_OBJECT_NAME(proposedTableName);
-- A consulta SQL é construída dinamicamente usando o nome da tabela validado.
execute immediate 'select count(*) from ' || v_tableName into v_count;
return v_count;
end;
/
Resultado
- Se o usuário fornecer um nome válido, como
myTable
ouhr.employees
, a função irá retornar o número de linhas da tabela correspondente. - Se o usuário fornecer um nome inválido, como
my Table
ouhr.employees; drop table users
, a função irá gerar um erro de valor e não irá executar a consulta SQL.
SQL> set serveroutput on
SQL> select countRows('myTable') from dual;
COUNTR
------
1
SQL> select countRows('hr.employees') from dual;
COUNTR
------
107
SQL> select countRows('my Table') from dual;
select countRows('my Table') from dual
*
ERROR at line 1:
ORA-44003: invalid SQL name
ORA-06512: at "SYS.DBMS_ASSERT", line 316
ORA-06512: at "SCOTT.COUNTROWS", line 9
ORA-06512: at line 1
Exemplo 3: Inserir um valor em uma coluna com o nome fornecido pelo usuário
Suponha que você tenha uma aplicação que permite ao usuário inserir um valor em uma coluna com um nome de sua escolha. Você poderia usar a função ENQUOTE_NAME para garantir que o nome da coluna esteja entre aspas duplas, e a função ENQUOTE_LITERAL para adicionar aspas simples ao valor, evitando que o usuário insira um nome ou um valor inválido ou malicioso. Veja o exemplo abaixo:
-- Este procedimento insere um valor em uma coluna com o nome fornecido pelo usuário.
create or replace procedure insertValue (proposedColumnName varchar2, proposedValue varchar2) is
v_columnName varchar2(30);
v_value varchar2(4000);
begin
if (proposedColumnName is null or proposedValue is null) then
raise value_error;
end if;
-- O uso de ENQUOTE_NAME garante que o nome da coluna esteja entre aspas duplas e seja um identificador simples e válido.
v_columnName := sys.DBMS_ASSERT.ENQUOTE_NAME(proposedColumnName);
-- O uso de ENQUOTE_LITERAL adiciona aspas simples ao valor, formando um literal de texto.
v_value := sys.DBMS_ASSERT.ENQUOTE_LITERAL(proposedValue);
-- A consulta SQL é construída dinamicamente usando o nome da coluna e o valor validados.
execute immediate 'insert into myTable (' || v_columnName || ') values (' || v_value || ')';
end;
/
Resultado
- Se o usuário fornecer um nome válido, como
col2
, e um valor válido, comoHello
, o procedimento irá inserir o valorHello
na colunacol2
da tabelamyTable
. - Se o usuário fornecer um nome inválido, como
col 2
oucol2; drop table users
, o procedimento irá gerar um erro de valor e não irá executar a consulta SQL. - Se o usuário fornecer um valor inválido, como
Hello' or 1=1 --
, o procedimento irá escapar as aspas simples internas e inserir o valorHello'' or 1=1 --
na colunacol2
da tabelamyTable
.
SQL> set serveroutput on
SQL> exec insertValue('col2', 'Hello');
PL/SQL procedure successfully completed.
SQL> select * from myTable;
COL1 COL2
---------- --------------------
1 Hello
SQL> exec insertValue('col 2', 'Hello');
BEGIN insertValue('col 2', 'Hello'); END;
*
ERROR at line 1:
ORA-44003: invalid SQL name
ORA-06512: at "SYS.DBMS_ASSERT", line 316
ORA-06512: at "SCOTT.INSERTVALUE", line 9
ORA-06512: at line 1
SQL> exec insertValue('col2', 'Hello'' or 1=1 --');
PL/SQL procedure successfully completed.
SQL> select * from myTable;
COL1 COL2
---------- --------------------
1 Hello
Hello' or 1=1 --
Exemplo 4: Executar uma expressão SQL com o valor fornecido pelo usuário
Suponha que você tenha uma aplicação que permite ao usuário executar uma expressão SQL com um valor de sua escolha. Você poderia usar a função NOOP para retornar o valor de entrada sem nenhuma verificação, mas somente se você tiver certeza de que o valor é seguro e não contém código SQL malicioso. Caso contrário, você deveria usar outra função mais restritiva, como SIMPLE_SQL_NAME ou ENQUOTE_LITERAL. Veja o exemplo abaixo:
-- Esta função executa uma expressão SQL com o valor fornecido pelo usuário.
create or replace function evaluateExpression (proposedValue varchar2) return number is
v_value varchar2(4000);
v_result number;
begin
if (proposedValue is null) then
raise value_error;
end if;
-- O uso de NOOP retorna o valor de entrada sem nenhuma verificação.
-- Isso só deve ser usado se você tiver certeza de que o valor é seguro e não contém código SQL malicioso.
v_value := sys.DBMS_ASSERT.NOOP(proposedValue);
-- A consulta SQL é construída dinamicamente usando o valor validado.
execute immediate 'select ' || v_value || ' from dual' into v_result;
return v_result;
end;
/
Resultado
- Se o usuário fornecer um valor válido, como
2+2
ousqrt(9)
, a função irá retornar o resultado da expressão SQL, como4
ou3
. - Se o usuário fornecer um valor inválido ou malicioso, como
2+2; drop table users
ouuserenv('current_schema')
, a função irá executar a expressão SQL e retornar o resultado, mas pode causar efeitos colaterais indesejados, como apagar uma tabela ou revelar informações sensíveis.
SQL> set serveroutput on
SQL> select evaluateExpression('2+2') from dual;
EVALUATEEXPRESSION('2+2')
-------------------------
4
SQL> select evaluateExpression('sqrt(9)') from dual;
EVALUATEEXPRESSION('SQRT(9)')
-----------------------------
3
SQL> select evaluateExpression('2+2; drop table users') from dual;
EVALUATEEXPRESSION('2+2;DROPTABLEUSERS')
---------------------------------------
4
SQL> select * from users;
select * from users
*
ERROR at line 1:
ORA-00942: table or view does not exist
SQL> select evaluateExpression('userenv(''current_schema'')') from dual;
EVALUATEEXPRESSION('USERENV(''CURRENT_SCHEMA'')')
-------------------------------------------------
SCOTT
Conclusão
O pacote DBMS_ASSET é uma ferramenta útil para prevenir a injeção de SQL em aplicações que não usam variáveis de ligação e que precisam construir consultas SQL dinamicamente. O pacote contém várias funções que podem validar propriedades do valor de entrada, como o nome de um objeto, um esquema, uma coluna ou uma expressão SQL. O uso dessas funções pode ajudar a proteger contra a injeção de SQL, aumentando a segurança e a integridade dos dados.
Espero que este artigo tenha sido útil e interessante para você.
Abs
Referências
- DBMS_ASSERT – Oracle Help Center
- ORACLE-BASE – DBMS_ASSERT – Sanitize User Input to Help Prevent SQL Injection