Input sanitization - Numeric values - oracle

I've been asked to do input validation in order to prevent sql injection. I've been using dbms assert package functions to do the sanitization. However, when I try to sanitize a number(I'm getting it in varchar2(12 byte)) error is thrown. It's the same case with alphanumeric characters starting with number.
I tried various functions of dbms assert. Nothing seems to work except noop. But, noop is of no use since it does not do any validation.
create or replace procedure employee
(
v_emp_id IN varchar2(12 byte)
)
AS
lv_query CLOB;
BEGIN
if v_emp_id is NOT NULL THEN
lv_query := 'select * from employee where emp_id=''' || dbms_assert.enquote_name(v_emp_id) || '''';
--I also tried below:
-- lv_query := 'select * from employee where emp_id=''' || dbms_assert.simple_sql_name(v_emp_id) || '''';
end if;
END
No source gives more detailed input on dbms_assert package. Please help me in
Whether dbms_assert package can be used to sanitize numeric values(stored in VARCHAR2 variables). If yes, how?
Other ways of sanitizing input. (other than using bind variables)
Thanks.

Oracle 12.2 and higher
If you are on Oracle 12.2 or higher, you can use the VALIDATE_CONVERSION function which would be the simplest solution. Your code could potentially look something like this:
CREATE OR REPLACE PROCEDURE employee (v_emp_id IN VARCHAR2)
AS
lv_query CLOB;
BEGIN
IF v_emp_id IS NOT NULL AND validate_conversion (v_emp_id AS NUMBER) = 1
THEN
lv_query := 'select * from employee where emp_id = ' || v_emp_id;
ELSE
--do something here with an invalid number
null;
END IF;
END;
/
Earlier than Oracle 12.2
If you are not on Oracle 12.2 or higher, you can write your own small function to validate that the value is a number. Using a method similar to what Belayer suggested, just attempt to convert the value to a number using the TO_NUMBER function and if it fails, then you know it's not a number. In my example, I have it as a small anonymous block within the code but you can also make it a standalone function if you wish.
CREATE OR REPLACE PROCEDURE employee (v_emp_id IN VARCHAR2)
AS
lv_query CLOB;
l_is_number BOOLEAN;
BEGIN
--Verify that the parameter is a number
DECLARE
l_test_num NUMBER;
BEGIN
l_test_num := TO_NUMBER (v_emp_id);
l_is_number := TRUE;
EXCEPTION
WHEN VALUE_ERROR
THEN
l_is_number := FALSE;
END;
--Finished verifying if the parameter is a number
IF v_emp_id IS NOT NULL AND l_is_number
THEN
lv_query := 'select * from employee where emp_id = ' || v_emp_id;
ELSE
--do something here with an invalid number
null;
END IF;
END;
/

Well if you cannot change the procedure it means you have no test as that procedure will not compile, so it cannot be executed. However that may be a moot point. You need to define exactly what you mean by "sanitize numeric values". Do you mean validate a string contains a numeric value. If so DBMS_ASSERT will not do that. (Note: The function chooses ENQUOTE_NAME will uppercase the string and put double quotes (") around it thus making it a valid object name.) Further your particular validation may require you define a valid numeric value, is it: an integer, a floating point, is scientific nation permitted, is there a required precision and scale that must be satisfied, etc. As a brute force validation you can simulate the assertion by just convert to number. The following will do that. Like dbms_assert if the assertion is successful it returns the input string. Unlike dbms_assert, however, when the assertion fails it just returns null instead of raising an exception. See fiddle.
create or replace
function assert_is_numeric(value_in varchar2)
return varchar2
is
not_numeric exception;
pragma exception_init (not_numeric,-06502);
l_numeric number;
begin
l_numeric := to_number(value_in);
return value_in;
exception
when not_numeric then
return null;
end assert_is_numeric;

Related

How can I test my package with one function?

I made a package that compiles fine but when I try to test it it gives me "invalid data type".
I've tried two different ways, first one like this
select pkg_contabilidad.f_totalizar_Detalle(100) FROM DUAL;
It gives me the ORA-00902 'invalid data type'
Also I've tried this
DECLARE
TYPE r_registro IS RECORD
(rubro_contable CN_RUBROS_CONTABLES.COD_RUBRO%TYPE,
tipo VARCHAR2(1),
monto NUMBER(16));
resultao r_registro;
numero NUMBER :=100;
BEGIN
resultao := pkg_contabilidad.f_totalizar_detalle(numero);
END;
It gives me another error PLS-00382 'expression is of wrong type'
I don't know what am I doing wrong, cause my function receives just one parameter and is of type NUMBER, so I dont know where's my mistake. I'll leave the code of my package just in case
CREATE OR REPLACE PACKAGE pkg_contabilidad AS
TYPE r_registro IS RECORD
(rubro_contable CN_RUBROS_CONTABLES.COD_RUBRO%TYPE,
tipo VARCHAR2(1),
monto NUMBER(16));
TYPE t_detalle IS TABLE OF
r_registro INDEX BY BINARY_INTEGER;
FUNCTION f_totalizar_detalle(p_clave NUMBER)RETURN t_detalle;
END pkg_contabilidad;
/
CREATE OR REPLACE PACKAGE BODY pkg_contabilidad AS
B_detalle t_detalle;
i integer :=1;
FUNCTION f_totalizar_detalle(p_clave NUMBER) RETURN t_detalle IS
v_detalle t_detalle;
CURSOR c_facturado IS
SELECT c.cod_rubro, 'H', CASE WHEN SUM(d.gravada)=0 THEN SUM(d.iva) ELSE SUM(d.gravada) END
FROM fn_documentos_det d JOIN fn_conceptos c ON d.cod_concepto = c.cod_concepto
WHERE d.clave_doc=p_clave
GROUP BY c.cod_rubro;
CURSOR c_datos IS
SELECT SUM(d.total_doc), 'D',r.cod_rubro
FROM fn_documentos d JOIN fn_cajas_ctas r ON d.num_caja_cta = r.num_caja_cta
WHERE d.clave_doc = p_clave
GROUP BY r.cod_rubro;
BEGIN
open c_datos;
LOOP
FETCH c_datos INTO v_detalle(1);
END LOOP;
CLOSE c_datos;
FOR fila IN c_facturado LOOP
i := i + 1;
v_detalle(i) := fila;
END LOOP;
END;
END PKG_CONTABILIDAD;
The function returns a pkg_contabilidad.t_detalle, so the test needs to be:
declare
resultao pkg_contabilidad.t_detalle;
numero number := 100;
begin
resultao := pkg_contabilidad.f_totalizar_detalle(numero);
end;
It doesn't work in SQL because pkg_contabilidad.t_detalle is a PL/SQL type, not a SQL type (create or replace type). The database can perform some automatic conversions, but there are still limitations.
By the way, this loop will never complete because it lacks an exit condition:
open c_datos;
loop
fetch c_datos into v_detalle(1);
end loop;
close c_datos;
Your function returns a PL/SQL table type, with a table of a PL/SQL record type, which is defined in your package, which plain SQL doesn't know about and can't display - hence your invalid datatype error. If you need to call the function and access the data from SQL you can create schema-level object and collection types instead.
In your anonymous block you are a declaring a new record type. That looks the same to you because the structure is the same, but Oracle expects the exact type the function returns. That makes your test code shorter and simpler though. But you are also trying to return the whole collection into a single record.
DECLARE
l_detalle pkg_contabilidad.t_detalle;
l_registro pkg_contabilidad.r_registro;
l_idx pls_integer;
numero NUMBER :=100;
BEGIN
l_detalle := pkg_contabilidad.f_totalizar_detalle(numero);
l_idx := l_detalle.FIRST;
WHILE l_idx is not null LOOP
l_registro := l_detalle(l_idx);
-- do something with this record
dbms_output.put_line(l_registro.tipo);
l_idx := l_detalle.NEXT(l_idx);
END LOOP;
END;
db<>fiddle with dummy cursors.
Your function is a bit strange and probably isn't doing quite what you want; but also has two fatal problems: it isn't returning anything, and it has an infinite loop. I've fixed those for the fiddle but not anything else, as this seems to be an exercise.

VARCHAR2(32767) not able to handle strings in stored procedure

I am concatenating string using cursor (to form query to execute later). Here, the query that will be formed is going to be way bigger that what VARCHAR2(32767) can handle. There fore, I am getting error on proc execution - ORA-06502: PL/SQL: numeric or value error: character string buffer too small.
I used CLOB data type as well bu got error ORA-06502: PL/SQL: numeric or value error.
My code is here below:
CREATE OR REPLACE PROCEDURE sp_Market
IS
Names VARCHAR2(32767);
BEGIN
DECLARE CURSOR cur IS ('Select ID, Order_of, field_name
FROM pld_medicare_config');
BEGIN
FOR i IN cur
LOOP
Names := Names || i.sqql;
END LOOP;
dbms_output.put_line(Names);
END;
END sp_Market;
How can I handle my string of queries and what data type is there to accomplish the task?
CLOB is OK (as far as I can tell); I doubt queries you store in there are that big.
Remove dbms_output.put_line call from the procedure; I suspect it is the one that raises the error.
I'm not sure how you got any runtime error, as your procedure won't compile.
The valid PL/SQL version would look something like this:
create or replace procedure sp_market is
names varchar2(32767);
begin
for r in (
select id, order_of, field_name
from pld_medicare_config
)
loop
names := names || ' ' || r.field_name;
end loop;
names := ltrim(names);
dbms_output.put_line(names);
end sp_market;
If names needs to be longer, change the datatype to clob.
Use the CLOB datatype and append data using the dbms_lob.writeappend procedure. This is the reference (Oracle 18c).
The error probably origins with the dbms_output.put_line call. The procedure is defined for varchar2 arguments only which means that an implicit conversion takes place during the call. It will fail for clob contents longer than 32767 chars/bytes.
Alternatively you may declare a collection over varchar2(4000) and fill the collection elements sequentially:
CREATE OR REPLACE PROCEDURE sp_Market
IS
TYPE tLongString IS TABLE OF VARCHAR2(4000) INDEX BY BINARY_INTEGER;
cNames tLongString;
BEGIN
DECLARE CURSOR cur IS Select ID, Order_of, field_name, sqql FROM pld_medicare_config;
BEGIN
FOR i IN cur
LOOP
cNames(cNames.COUNT+1) := i.sqql;
END LOOP;
END;
END sp_Market;
Note
Rectified code, will compile now.

How can this piece of PLSQL be made to compile?

I need to return the names of employees in string format for all those employees whose manager ID depends on the passed parameter. When I compile the function I get an error. Here is the function code:
create or replace function Employee(v_manid IN employees.manager_id%type)
return varchar2
AS
cursor cur_emp is select last_name from employees where manager_id = v_manid;
v_names varchar2(10);
begin
for emp_rec in cur_emp
loop
v_name = v_name || emp_rec.last_name ||', ';
end loop;
return v_name
end;
/
The error is:
Error(8,8): PLS-00103: Encountered the symbol "=" when expecting one
of the following: := . ( # % ; Error(8,44): PLS-00103:
Encountered the symbol ";" when expecting one of the following: )
, * & - + / at mod remainder rem and or ||
Could anyone help me with this?
As stated in the other answers the reason why your function won't compile is threefold.
You've declared the variable v_names and are referencing it as v_name.
The assignment operator in PL/SQL is :=, you're using the equality operator =.
You're missing a semi-colon in your return statement; it should be return v_name;
It won't stop the function from compiling but the variable v_names is declared as a varchar2(10). It's highly unlikely that when a manager with multiple subordinates all their last names will fit into this. You should probably declare this variable with the maximum size; just in case.
I would like to add that you're doing this a highly inefficient way. If you were to do the string aggregation in SQL as opposed to a PL/SQL loop it would be better. From 11g release 2 you have the listagg() function; if you're using a version prior to that there are plenty of other string aggregation techniques to achieve the same result.
create or replace function employee ( p_manid in employees.manager_id%type
) return varchar2 is
v_names varchar2(32767); -- Maximum size, just in case
begin
select listagg(lastname, ', ') within group ( order by lastname )
into v_names
from employees
where manager_id = p_manid;
return v_names;
exception when no_data_found then
return null;
end;
/
Please note a few other changes I've made:
Prepend a different letter onto the function parameter than the variable to make it clear which is which.
Add in some exception handling to deal with there being no data for that particular manager.
You would have returned , if you had no data I return NULL. If you want to return a comma instead simply put this inside the exception.
Rather than bother to create a cursor and loop through it etc I let Oracle do the heavy lifting.
It's rather curious that you would want to return a comma delimited list as there is little that you would be able to do with it in Oracle afterwards. It might be more normal to return something like an array or an open cursor containing all the surnames. I assume, in this answer, that you have a good reason for doing what you are.
There are a couple of things to be noted.
Declared as v_names but used as v_name
Assignemnt should be like v_name := v_name || emp_rec.last_name ||
', ';
v_name is declared with size of 10, it would be too small and would
give an error when you execute, so you could declare as
v_name employees.last_name%TYPE;
You could create your function as
CREATE OR REPLACE FUNCTION employee (v_manid IN employees.manager_id%TYPE)
RETURN VARCHAR2
AS
v_name employees.last_name%TYPE;
CURSOR cur_emp
IS
SELECT last_name
FROM employees
WHERE manager_id = v_manid;
BEGIN
FOR emp_rec IN cur_emp
LOOP
v_name := v_name || emp_rec.last_name || ', ';
END LOOP;
RETURN v_name;
END;
/
I guess you should use := instead of =
like
v_name := v_name || emp_rec.last_name ||', ';
one more thing you also need to add semicolon ; at the end of return v_name like
return v_name;

"Boolean" parameter for Oracle stored procedure

I'm aware that Oracle does not have a boolean type to use for parameters, and am currently taking in a NUMBER type which would have 1/0 for True/False (instead of the 'Y'/'N' CHAR(1) approach).
I'm not a very advanced Oracle programmer, but after doing some digging and reading some ASKTOM posts, it seems like you can restrict a field using a format for the column like:
MyBool NUMBER(1) CHECK (MyBool IN (0,1))
Is there a way to apply the same sort of a check constraint to an input parameter to a stored procedure? I'd like to restrict the possible inputs to 0 or 1, rather than checking for it explicitly after receiving the input.
You can use Booleans as parameters to stored procedures:
procedure p (p_bool in boolean) is...
However you cannot use Booleans in SQL, e.g. select statements:
select my_function(TRUE) from dual; -- NOT allowed
For a number parameter there is no way to declaratively add a "check constraint" to it, you would have to code some validation e.g.
procedure p (p_num in number) is
begin
if p_num not in (0,1) then
raise_application_error(-20001,'p_num out of range');
end if;
...
Yes and no.
You can do..
create or replace package t_bool is
subtype t_bool_num IS PLS_INTEGER RANGE 0..1;
function f_test (i_bool_num t_bool_num) return varchar2;
end t_bool;
/
create or replace package body t_bool is
function f_test (i_bool_num t_bool_num) return varchar2 is
begin
if i_bool_num = 0 then
return 'false';
elsif i_bool_num = 1 then
return 'true';
elsif i_bool_num is null then
return 'null';
else
return to_char(i_bool_num);
end if;
end;
end t_bool;
/
The good news is that, if you do
exec dbms_output.put_line(t_bool.f_test(5));
it reports an error.
The bad news is that if you do
select t_bool.f_test(5) from dual;
then you don't get an error

How do I unit test this PL/SQL procedure? (using utplsql)

Package is very very basic.
Loops through a cursor, and updates 2 values where the record_ids are equal.
What's an appropriate unit test for this sort of procedure?
I'm going to add some skeleton code because the answers so far, while good, tie to the crux of my issue here: What do I test?
PROCEDURE set_shift_times_to_null( RETVAL OUT VARCHAR2,
ERRBUF OUT VARCHAR2,
RECORDS_UPDATED OUT NUMBER) IS
CURSOR evening_shift_employees_cur IS
select employee
FROM employees
where SHIFT='EVENING'
;
BEGIN
RECORDS_UPDATED := 0;
RETVAL := '2';
FOR evening_shift_employees IN evening_shift_employees_cur LOOP
UPDATE NIGHT_SHIFT
Set SOME_DUMB_FIELD = evening_shift_employees.employee;
RECORDS_UPDATED := RECORDS_UPDATED + 1;
END LOOP;
COMMIT;
RETVAL := 0;
EXCEPTION WHEN OTHERS THEN
ROLLBACK;
ERRBUF := 'Error occurred - ' || SQLERRM;
END set_shift_times_to_null;
A couple of suggestions.
Use SQL%ROWCOUNT:
BEGIN
UPDATE NIGHT_SHIFT
Set SOME_DUMB_FIELD = evening_shift_employees.employee;
v_rows_processed := SQL%ROWCOUNT;
dbms_output.put_line('There were '||v_rows_processed||' rows updated');
END;
Don't Use When Others (why do you want to lose the stack trace).Just use Exceptions, you will be relying on the caller to check the contents of the ERRBUF.
begin
insert into t values ( 1 );
exception when others then
log_error;
raise;
end;
log_error implementation looks like:
create or replace procedure log_error
as
pragma autonomous_transaction;
l_whence varchar2(1024);
l_msg varchar2(1020) default sqlerrm;
l_code number default sqlcode;
begin
l_whence := whence;
insert into error_table
( timestamp, whence, msg, code )
values
( sysdate, whence, l_msg, l_code );
commit;
exception
when others then
rollback;
raise;
end;
Consider not using any pl/sql. on the surface the update appears completely 'doable' without any cursor. Perhaps an updateable inline view:
update (
select e.sal as emp_sal, e.comm as emp_comm,
ns.sal as ns_sal, ns.sal/2 as ns_comm
from employees e, night_shift ns
where e.deptno = ns.deptno
)
set emp_sal = ns_sal, emp_comm = ns_comm
The appropriate unit test it to validate the affected tables to check that the updated records are what expected.
You can create temporary tables with the results you expect and the unit testing code compare the results. Of course is hard work but if you want to test you have to do something like this.
It depends on the work of procedure, but if you want to be sure that test is fine you have to check as possibilities as possible.
A lot of the conditions have to be validated with constraints and the test unit procedures have to execute code that force the database to check that constraints (inserts, and so on).
What I ended up doing was the following:
Take record count of records that main cursor will update fields to null
execute procedure ( returns a value of rows updated )
Take record count of same records as first time
The difference between the record counts should equal the number of records updated in the procedure. If so, the test passes.
Does this make sense, or is it circular logic?
For anyone else who sees this, I found this in the documentation for utplsql:
PROCEDURE utAssert.eqtable (
msg_in IN VARCHAR2,
check_this_in IN VARCHAR2,
against_this_in IN VARCHAR2,
check_where_in IN VARCHAR2 := NULL,
against_where_in IN VARCHAR2 := NULL,
raise_exc_in IN BOOLEAN := FALSE
);
It's under the assert documentation; looks like it does exactly what I was trying to do.
Basically, you want to exercise all the possibilities of your procedure:
Test with equal record-ids.
Test with non-equal record-ids.
Test with invalid (non-integer? negative?) record-ids.
Also, test the boundary conditions:
Test with record-ids off by one (ex: 104 and 105).
Test with maximum record-id (MAX_INT?).
Test with zero-value record-id.
Here is a nice example of good unit-testing practices.
EDIT: I don't know of a robust unit-testing tool for database queries. I would set up a test table of evening_shift_employees with various record-id conditions as described above. Then, as suggested by FerranB, check that the records are updated as expected for validation.

Resources