August 2, 2010
Last Friday I received my copy of the new book “Expert Oracle Database Architecture: Oracle Database 9i, 10g, and 11g Programming Techniques and Solutions“. There is an interesting demonstration of SQL injection on pages 17 through 21 of the book, where a clever use of SQL injection permits a significant amount of information to be returned when the only supplied value for a SQL statement is declared as a DATE data type. If you bought the book, take a close look at the example on those pages (I do not recall this example from the first edition of this book or his “Expert One On One” book). Certainly, you have heard of Bobby Tables?
I thought that it might be interesting to create a slight twist (an alternate ending) for the example shown in the book. Let’s create a simple test table with a list of employees, divisions, and series of dates:
CREATE TABLE T1 ( EMPLOYEE_ID VARCHAR2(30), DIVISION VARCHAR2(30), SALES_DATE DATE); INSERT INTO T1 SELECT DECODE(MOD(ROWNUM,10), 1,'JIM', 2,'JACK', 3,'MIKE', 4,'BOB', 5,'JOE', 6,'SUE', 7,'MARY', 8,'JOAN', 9,'CAROL', 0,'JAN'), DECODE(MOD(ROWNUM,6), 0,'DIVISION 6', 'DIVISION '||TO_CHAR(MOD(ROWNUM,6))), TRUNC(SYSDATE)-1000+ROWNUM FROM DUAL CONNECT BY LEVEL<=1000; COMMIT;
Now assume that we have a SQL statement like this in a stored procedure:
SELECT * FROM T1 WHERE EMPLOYEE_ID='JIM' AND SALES_DATE>='01-JUL-2010' ORDER BY SALES_DATE; EMPLOYEE_ID DIVISION SALES_DAT ----------- ----------- --------- JIM DIVISION 5 04-JUL-10 JIM DIVISION 3 14-JUL-10 JIM DIVISION 1 24-JUL-10
So, for the employee JIM there were three sales dates, one each in DIVISION 5, DIVISION 3, and DIVISION 1. The stored procedure might look something like this (adapted from the example in the book):
CREATE OR REPLACE PROCEDURE T1_RETRIEVE (strEmployeeID IN VARCHAR,dteSalesDate IN DATE) AS strSQL VARCHAR(1000); rowT1 T1%ROWTYPE; curT1 SYS_REFCURSOR; begin strSQL := ' SELECT * FROM T1 WHERE EMPLOYEE_ID=''' || strEmployeeID ||''' AND SALES_DATE>=''' || dteSalesDate ||''' ORDER BY 3'; dbms_output.put_line( strSQL ); open curT1 for strSQL; loop fetch curT1 into rowT1; exit when curT1%notfound; dbms_output.put_line ( rowT1.EMPLOYEE_ID || ',' || rowT1.DIVISION || ',' || TO_CHAR(rowT1.SALES_DATE,'MM/DD/YYYY')); end loop; close curT1; end; /
We are able to verify that the stored procedure works like this:
SET SERVEROUTPUT ON SIZE UNLIMITED
EXEC T1_RETRIEVE('JIM','01-JUL-2010')
SELECT
*
FROM
T1
WHERE
EMPLOYEE_ID='JIM'
AND SALES_DATE>='01-JUL-2010'
ORDER BY 3
JIM,DIVISION 5,07/04/2010
JIM,DIVISION 3,07/14/2010
JIM,DIVISION 1,07/24/2010
It appears that the stored procedure worked. Maybe I was too harsh in the book review of “Oracle SQL Recipes: A Problem – Solution Approach“? It certainly appears as though we are relying on implicit data type conversion.
Let’s assume that someone is interested in bypassing the date and employee specification for this stored procedure, so that all rows are returned. Of course it is important to hide what is changed. Maybe we would want the result of the SQL injection to look something like this:
SELECT * FROM T1 WHERE EMPLOYEE_ID='JIM' AND SALES_DATE>='' UNION ALL SELECT * FROM T1; EMPLOYEE_ID DIVISION SALES_DAT ----------- ----------- --------- JIM DIVISION 1 07-NOV-07 JACK DIVISION 2 08-NOV-07 MIKE DIVISION 3 09-NOV-07 BOB DIVISION 4 10-NOV-07 JOE DIVISION 5 11-NOV-07 SUE DIVISION 6 12-NOV-07 MARY DIVISION 1 13-NOV-07 JOAN DIVISION 2 14-NOV-07 CAROL DIVISION 3 15-NOV-07 JAN DIVISION 4 16-NOV-07 JIM DIVISION 5 17-NOV-07 ...
For instance, if we do this:
ALTER SESSION SET NLS_DATE_FORMAT = '"''UNION ALL SELECT * FROM T1 --"';
Now if we execute the stored procedure like before:
EXEC T1_RETRIEVE('JIM','01-JUL-2010')
Output:
-----------------------
ERROR at line 1:
ORA-01861: literal does not match format string
ORA-06512: at line 1
-----------------------
An error? I did provide a warning in the review of the SQL Recipes book. Let’s do it the correct way this time to avoid the implicit data type conversion:
EXEC T1_RETRIEVE('JIM',TO_DATE('01-JUL-2010','DD-MON-YYYY'))
SELECT
*
FROM
T1
WHERE
EMPLOYEE_ID='JIM'
AND SALES_DATE>=''UNION ALL SELECT * FROM T1 --'
ORDER BY 3
JIM,DIVISION 1,11/07/2007
JACK,DIVISION 2,11/08/2007
MIKE,DIVISION 3,11/09/2007
BOB,DIVISION 4,11/10/2007
JOE,DIVISION 5,11/11/2007
SUE,DIVISION 6,11/12/2007
MARY,DIVISION 1,11/13/2007
JOAN,DIVISION 2,11/14/2007
CAROL,DIVISION 3,11/15/2007
JAN,DIVISION 4,11/16/2007
JIM,DIVISION 5,11/17/2007
JACK,DIVISION 6,11/18/2007
MIKE,DIVISION 1,11/19/2007
BOB,DIVISION 2,11/20/2007
JOE,DIVISION 3,11/21/2007
SUE,DIVISION 4,11/22/2007
MARY,DIVISION 5,11/23/2007
JOAN,DIVISION 6,11/24/2007
CAROL,DIVISION 1,11/25/2007
JAN,DIVISION 2,11/26/2007
JIM,DIVISION 3,11/27/2007
JACK,DIVISION 4,11/28/2007
MIKE,DIVISION 5,11/29/2007
...
MARY,DIVISION 1,07/30/2010
JOAN,DIVISION 2,07/31/2010
CAROL,DIVISION 3,08/01/2010
JAN,DIVISION 4,08/02/2010
That certainly is a different result than what we received the first time the procedure was executed (what the programmer probably intended). We could have also tried this:
EXEC T1_RETRIEVE('JIM',SYSDATE)
I suppose that things might have been worse if Bobby Tables had a chance to experiment with this example. We really should be using bind variables in this example, but what if you still want to use literals rather than bind variables – is there a work-around (one might be wondering if it is not easier to just do things the right way from the start)? Maybe if we remove the source of the implicit data type conversion in the procedure like this:
CREATE OR REPLACE PROCEDURE T1_RETRIEVE (strEmployeeID IN VARCHAR,dteSalesDate IN DATE) AS
strSQL VARCHAR(1000);
rowT1 T1%ROWTYPE;
curT1 SYS_REFCURSOR;
begin
strSQL := '
SELECT
*
FROM
T1
WHERE
EMPLOYEE_ID=''' || strEmployeeID ||'''
AND SALES_DATE>=TO_DATE(''' || dteSalesDate ||''', ''DD-MON-YYYY'')
ORDER BY 3';
dbms_output.put_line( strSQL );
open curT1 for strSQL;
loop
fetch curT1 into rowT1;
exit when curT1%notfound;
dbms_output.put_line (
rowT1.EMPLOYEE_ID || ',' || rowT1.DIVISION || ',' || TO_CHAR(rowT1.SALES_DATE,'MM/DD/YYYY'));
end loop;
close curT1;
end;
/
Now testing the procedure:
ALTER SESSION SET NLS_DATE_FORMAT='DD-MON-YYYY';
EXEC T1_RETRIEVE('JIM','01-JUL-2010')
SELECT
*
FROM
T1
WHERE
EMPLOYEE_ID='JIM'
AND SALES_DATE>=TO_DATE('01-JUL-2010', 'DD-MON-YYYY')
ORDER BY 3
JIM,DIVISION 5,07/04/2010
JIM,DIVISION 3,07/14/2010
JIM,DIVISION 1,07/24/2010
PL/SQL procedure successfully completed.
OK, it works. Now what happens when the attempted SQL injection happens:
ALTER SESSION SET NLS_DATE_FORMAT = '"''UNION ALL SELECT * FROM T1 --"';
EXEC T1_RETRIEVE('JIM',TO_DATE('01-JUL-2010','DD-MON-YYYY'))
SELECT
*
FROM
T1
WHERE
EMPLOYEE_ID='JIM'
AND SALES_DATE>=TO_DATE(''UNION ALL SELECT * FROM T1 --', 'DD-MON-YYYY')
ORDER BY 3
BEGIN T1_RETRIEVE('JIM',TO_DATE('01-JUL-2010','DD-MON-YYYY')); END;
*
ERROR at line 1:
ORA-00907: missing right parenthesis
ORA-06512: at "TESTUSER.T1_RETRIEVE", line 18
ORA-06512: at line 1
That’s better, but now we created a new problem… time for another change to the procedure?

At the end, it all boils down to sanitzing the input into the database. Every attack to the database comes through the input data, and as there are many variations of the input data, databases need the help of developer to make sure that only “nice” data is being pumped into the database.
@Fahd:
Nope. It boils down to using bind variables. Only if you cannot use bind variables should you worry about sanitizing that small amount of input data.
@Charles:
Well, until you stop doing implicit date-to-string conversion, you’ll be vulnerable to this attack… How about a to_char(dteSalesDate,’DD-MON-YYYY’) in the last version?