Skip to Main Content
  • Questions
  • Possible to update a table; given a tablename and field values passed to a stored proc?

Breadcrumb

Question and Answer

Tom Kyte

Thanks for the question, Janice.

Asked: July 18, 2007 - 11:31 am UTC

Last updated: August 06, 2007 - 11:39 am UTC

Version: 9.2.0.8.0

Viewed 1000+ times

You Asked

Hi Tom,

I have a challenge for you - well maybe. At least it's a challenge for me. Is it possible to create a stored procedure that can update a single row(all fields) for any given table? The idea is to pass two parameters; 1.) a tablename and 2.) an array of field values to be updated.

What we have is a web application that is entirely database driven (even normally persistent layout such as text boxes, links, etc). Table structures vary based on each customer, and then by templates they have defined for specific uses.

Is it possible in the Oracle SP to update the table based on the passed in tablename string? I'm guessing that it will probably have to be dynamic SQL within the SP for this to work?

Would appreciate any insight you can provide.

Thanks,
Jan

and Tom said...

I could easily do this, but it would be a disservice to you and your database.

APEX (the software used to build asktom here for example) is the same as you describe - but it doesn't need to resort to "tricks" like this.


You have no datatypes - you only have strings if you do this.
You'll be doing dynamic SQL all over the place.
You could be subject to SQL Injection pretty easily.

You'd need to pass more than a tablename, you need a way to FIND the row.

What about lost updates??? Aren't you worried about that?

Generic code is "cool"
Generic code is generally pretty poor performing code, non-scalable code.


Here is an approach, I don't like it (can you tell by the name :)

I've started saying for the last couple of years - if you are doing dynamic sql and not binding (cannot bind identifiers for example), then you need to submit your code to at least 5 people that do not like you and have them try to break it with sql injection.

I spent a couple of minutes thinking about how this could be injected - and the trouble spots would be in the identifiers - the tablename, primary key name, and column names. I'm using a query against user_tab_columns to validate that the tablename and primary key column name are "valid" (we'll get no_data_found and the routine will bail if they do not exist). I'm using dbms_assert to verify that the column names are valid to be used as identifiers (doesn't mean they are column names - just that they could be column names - and hence are not rouge SQL bits)

Hey everyone, try to break it (please)

ops$tkyte%ORA9IR2> create or replace type Array as table of varchar2(4000)
  2  /

Type created.

ops$tkyte%ORA9IR2> create or replace context my_ctx using bad_idea
  2  /

Context created.

ops$tkyte%ORA9IR2>
ops$tkyte%ORA9IR2> create or replace
  2  function bad_idea
  3  ( p_tname in varchar2,
  4    p_pkname in varchar2,
  5    p_pkval in varchar2,
  6    p_cnames   in array,
  7    p_new_data in array
  8  )
  9  return number
 10  as
 11     l_data_type varchar2(30);
 12     l_stmt      long;
 13  begin
 14     select decode( data_type, 'DATE', 'TO_DATE(', 'NUMBER', 'TO_NUMBER(', '(' )
 15       into l_data_type
 16       from user_tab_columns
 17      where table_name = p_tname
 18        and column_name = p_pkname;
 19
 20     l_stmt := 'update ' || p_tname || ' set ';
 21     for i in 1 .. p_cnames.count
 22     loop
 23        l_stmt := l_stmt || ' ' ||
 24                  dbms_assert.SIMPLE_SQL_NAME( p_cnames(i) ) ||
 25                '=sys_context(''my_ctx'',''' || p_cnames(i) || ''' ),';
 26        dbms_session.set_context( 'my_ctx', p_cnames(i), p_new_data(i) );
 27     end loop;
 28     l_stmt := rtrim(l_stmt,',') || ' where ' ||
 29                dbms_assert.SIMPLE_SQL_NAME( p_pkname ) ||
 30                '=' || l_data_type || 'sys_context(''my_ctx'',''' || p_pkname || ''' ))';
 31     dbms_session.set_context( 'my_ctx', p_pkname, p_pkval );
 32
 33     dbms_output.put_line( l_stmt );
 34     execute immediate l_stmt;
 35     return sql%rowcount;
 36  end;
 37  /

Function created.

ops$tkyte%ORA9IR2>
ops$tkyte%ORA9IR2> create table t ( x int primary key, y varchar2(4), z date );

Table created.

ops$tkyte%ORA9IR2> insert into t values ( 1, 'x', sysdate );

1 row created.

ops$tkyte%ORA9IR2>
ops$tkyte%ORA9IR2> alter session set nls_date_format = 'yyyymmddhh24miss';

Session altered.

ops$tkyte%ORA9IR2> select * from t;

         X Y    Z
---------- ---- --------------
         1 x    20070719141152

ops$tkyte%ORA9IR2> declare
  2     l_new_data array := array('y',to_char(sysdate+1/24,'yyyymmddhh24miss') );
  3  begin
  4     dbms_output.put_line( bad_idea( 'T', 'X', 1, array('y','z'), l_new_data ) );
  5  end;
  6  /
update T set  y=sys_context('my_ctx','y' ), z=sys_context('my_ctx','z' ) where
X=TO_NUMBER(sys_context('my_ctx','X' ))
1

PL/SQL procedure successfully completed.

ops$tkyte%ORA9IR2> select * from t;

         X Y    Z
---------- ---- --------------
         1 y    20070719151152

Rating

  (3 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

Should have mentioned...

Janice Vann, July 19, 2007 - 3:05 pm UTC

Sorry Tom, I should have mentioned that there is a middle tier in the application where the sp would be invoked. The user's have no way of submitting a query directly to the database.

Nonetheless, I'm inclined to agree. The application is terribly designed but redesign is not an option at this point due to the number of interdependent pieces and resources available to do it.

Crystal ball

Gary, July 19, 2007 - 8:41 pm UTC

If "p_pkname" is supposed to be a/the primary key column, it would be safer to check user_cons_columns (mostly to prevent mistaken or intentional 'data destruction/data lock' type attack doing massive updates to tables). Adding a 'ROWNUM =1' predicate would be an alternative guard against that.
You defined array using varchar2(4000) where sys_context will by default only return the first 256 bytes.
I can't see a break though. Maybe if you had columns with user-defined datatypes and methods which had their own vulnerabilities.
An issue with this sort of procedure is that you are not just trying to prevent attacks on your current environment, but also what your environment may be in six months or years time. So here, you haven't listed the tables which you expect to be updated by the procedure (and I accept this is an example). In six months time you may add a new table to the same schema and not realise that users with execute for this procedure automatically gain updates to that table.
Tom Kyte
July 20, 2007 - 8:04 am UTC

very good input - appreciate it.

Identifier not quoted

Gary, August 05, 2007 - 10:27 pm UTC

You don't quote the table name input parameter in the update statement. It could be exploitable vulnerability only in a VERY weird scenario. Imagine if this code is in SCOTT and SCOTT has table "fred".
The attacker could create a view in his schema on a table (EMP) in his target schema (HR) and a public synonym FRED for that view.
Passing in the varchar2 value 'fred' would pick up SCOTT's table in the user_tab_columns check, but the update SQL doesn't quote the identifier so will pick up the public synonym and associated view for the update and try to update HR's EMP table with SCOTT's privileges.
It's a pretty miniscule vulnerability as it requires a lot of other conditions. However I though I'd highlight it as an additional check that should be considered for any dynamic SQL.
Tom Kyte
August 06, 2007 - 11:39 am UTC



reason 1123423 why sql injection is insidious

More to Explore

PL/SQL demos

Check out more PL/SQL tutorials on our LiveSQL tool.

PL/SQL docs

PL/SQL reference manual from the Oracle documentation library