Using string in Oracle stored procedure - oracle

When I simply write a query in which it has code like
Select * from ..
where ...
AND gfcid in ( select regexp_substr('1005771621,1001035181'||',','\d+',1,level)
from dual
connect by level <= (select max(length('1005771621,1001035181')-length(replace('1005771621,1001035181',',')))+1
from dual) )
It works.
But I want to make it dynamic query in oracle stored procedure. I did like this:
GDFCID_STRING := ' select regexp_substr('
|| '1005771621,1001035181'
|| ','
|| ','
|| '\d+'
|| ',1,level) from dual connect by level <= (select max(length('
|| '1005771621,1001035181'
|| ')-length(replace('
|| '1005771621,1001035181'
|| ','
|| ','
|| ')))+1 from dual)';
Select * from ..
where ...
AND gfcid in (GDFCID_STRING)
But it does now work.

As far as I understand your problem, you need a method to accept a comma-delimited string as an input, break it into a collection of integers and then compare a number (read: integer) with the values in this collection.
Oracle offers mainly three types of collections- varrays, nested tables and associative arrays. I would explain how to convert a comma-delimited string into a nested table and use it to query or compare.
First, you need to define an object type in the schema. You can write queries using this type only if you define it at schema level.
CREATE OR REPLACE TYPE entity_id AS OBJECT (id_val NUMBER(28));
/
CREATE OR REPLACE TYPE entity_id_set IS TABLE OF entity_id;
/
Next, define a function like this:
FUNCTION comma_to_nt_integer (p_comma_delimited_str IN VARCHAR)
RETURN entity_id_set IS
v_table entity_id_set;
BEGIN
WITH temp AS (SELECT TRIM(BOTH ',' FROM p_comma_delimited_str) AS str FROM DUAL)
SELECT ENTITY_ID(TRIM (REGEXP_SUBSTR (t.str,
'[^,]+',
1,
LEVEL)))
str
BULK COLLECT INTO v_table
FROM temp t
CONNECT BY INSTR (str,
',',
1,
LEVEL - 1) > 0;
RETURN v_table;
END comma_to_nt_integer;
You are done with the DDL required for this task. Now, you can simply write your query as:
SELECT *
FROM ..
WHERE ...
AND gfcid in (table(comma_to_nt_integer(GDFCID_STRING)));

In general you can use
execute immediate v_your_sql_code;
to execute dynamic SQL within PL/SQL, but from your question I'm not really aware what you want to do.
Edit:
y_your_sql_code := 'Select yourColumn from .. where ...AND gfcid in ('||GDFCID_STRING||')';
execute immediate v_your_sql_code into v_result;
You'll have to define v_result in the right datatype, you could use more then one result variable if you need more result columns, you'll need i.e. a complex type if you can retrieve more than one row.

Related

concatenate VArray to String and use in dynamic SQL - Oracle

I need to dynamically create a query and execute using Execute Immediate, I am facing the problem in appending the Vaaray variable. Getting Error
pls-00306 wrong number or types of arguments in call to ||
Vaaray //Its a type number
select ver_id bulk collect into Ver_Array from ( Select id from table)
No issue with below query as only id variable is used:
Execute Immediate 'Select ID, name , Date, time
from table
where id = ' || v_UC2_id
Error with below query
Execute Immediate 'Select ID, name , Date, time
from table
where id = ' || v_UC2_id
|| ' and ver_id in ( SELECT * FROM TABLE ( '
|| Ver_Array
|| ' )'
Tried to extract the query and concatenate in comma saperated values but the final result comes as String but field used in query is Number
Not sure how to handle this in dynamic query
The SQL you're writing is concatenating an array with a string, so you get an error.
You can do it like this:
create or replace type vat is varray(10) of number:
/
declare
ivat vat:=vat(1,2,3);
res number;
begin
execute immediate 'select sum(rn) from tarr where rn in (select column_value from table (:varrt))' into res using ivat;
dbms_output.put_line(res);
end;
/
Here I'm selecting just one row and value. If you have multiple rows and columns, then you better declare a cursor for this SQL, and loop thru it.

Creating SQL-Injection proof dynamic where-clause from collection in PL/SQL

I need to execute a query where the where-clause is generated based on user input. The input consists of 0 or more pairs of varchar2s.
For example:
[('firstname','John')
,('lastname','Smith')
,('street','somestreetname')]
This would translate into:
where (attrib = 'firstname' and value = 'John')
and (attrib = 'lastname' and value = 'Smith')
and (attrib = 'street' and value = 'somestreetname')
This is not the actual data structure as there are several tables but for this example lets keep it simple and say the values are in 1 table. Also I know the parentheses are not necessary in this case but I put them there to make things clear.
What I do now is loop over them and concatinate them to the SQL string. I made a stored proc to generate this where-clause which might also not be very secure since I just concat to the original query.
Something like the following, where I try to get the ID's of the nodes that correspond with the requested parameters:
l_query := select DISTINCT n.id from node n, attribute_values av
where av.node_id = n.id ' || getWhereClause(p_params)
open l_rc
for l_query;
fetch l_rc bulk collect into l_Ids;
close l_rc;
But this is not secure so I'm looking for a way that can guaranty security and prevent SQL-Injection attacks from happening.
Does anyone have any idea on how this is done in a secure way? I would like to use bindings but I don't see how I can do this when you dont know the number of parameters.
DB: v12.1.0.2 (i think)
It's still a bit unclear and generalised, but assuming you have a schema-level collection type, something like:
create type t_attr_value_pair as object (attrib varchar2(30), value varchar2(30))
/
create type t_attr_value_pairs as table of t_attr_value_pair
/
then you can use the attribute/value pairs in the collection for the bind:
declare
l_query varchar2(4000);
l_rc sys_refcursor;
type t_ids is table of number;
l_ids t_ids;
l_attr_value_pairs t_attr_value_pairs;
-- this is as shown in the question; sounds like it isn't exactly how you have it
p_params varchar2(4000) := q'^[('firstname','John')
,('lastname','Smith')
,('street','somestreetname')]^';
begin
-- whatever mechanism you want to get the value pairs into a collection;
-- this is just a quick hack to translate your example string
select t_attr_value_pair(rtrim(ltrim(
regexp_substr(replace(p_params, chr(10)), '(.*?)(,|$)', 1, (2 * level) - 1, null, 1),
'[('''), ''''),
rtrim(ltrim(
regexp_substr(replace(p_params, chr(10)), '(.*?)(,|$)', 1, 2 * level, null, 1),
''''), ''')]'))
bulk collect into l_attr_value_pairs
from dual
connect by level <= regexp_count(p_params, ',') / 2 + 1;
l_query := 'select DISTINCT id from attribute_values
where (attrib, value) in ((select attrib, value from table(:a)))';
open l_rc for l_query using l_attr_value_pairs;
fetch l_rc bulk collect into l_ids;
close l_rc;
for i in 1..l_ids.count loop
dbms_output.put_line('id ' || l_ids(i));
end loop;
end;
/
although it doesn't need to be dynamic with this approach:
...
begin
-- whatever mechamism you want to get the value pairs into a collection
...
select DISTINCT id
bulk collect into l_ids
from attribute_values
where (attrib, value) in ((select attrib, value from table(l_attr_value_pairs)));
for i in 1..l_ids.count loop
dbms_output.put_line('id ' || l_ids(i));
end loop;
end;
/
or with a join to the table collection expression:
select DISTINCT av.id
bulk collect into l_ids
from table(l_attr_value_pairs) t
join attribute_values av on av.attrib = t.attrib and av.value = t.value;
Other collection types will need different approaches.
Alternatively, you could still build up your where clause with one condition per attribute/value pair, while still making them bind variables - but you would need two levels of dynamic SQL, similar to this.

Is this safe against SQL injection?

Is the below form safe against SQL injection? I have to do this because the p_select_statement would be a simple select * from table where lastModifiedTime > :p_asof and <conditions>, but the code that calls getByFilter expects the columns to be in the order col1, col2, col3 while select * may return col2, col3, col1
OPEN CURSOR <dynamic_select_statement> USING <bind variable>
Example...
PROCEDURE getByFilter (
p_select_statement IN VARCHAR2,
p_asof IN TIMESTAMP,
p_cur OUT SYS_REFCURSOR
) AS
full_select_statement VARCHAR2(32767);
BEGIN
full_select_statement := 'SELECT
col1,
col2,
col3
FROM (' || p_select_statement || ')';
OPEN p_cur FOR full_select_statement USING p_asof;
END getByFilter;
No, not if the conditions can be set dynamically:
VARIABLE cur REFCURSOR;
DECLARE
conditions VARCHAR2(4000)
:= '1 = 0 UNION ALL '
|| 'select username AS col1, password AS col2, other_column AS col3 '
|| 'FROM your_secret_password_table';
sql VARCHAR2(4000)
:= 'select * from table where lastModifiedTime > :p_asof and ' || conditions;
BEGIN
PROCEDURE getByFilter (
p_select_statement => sql,
p_asof => SYSTIMESTAMP,
p_cur => :curr
);
END;
/
PRINT cur;
Yes, your example is clearly an example of SQL injection. It counts as SQL injection if your procedure executes some input verbatim as a query (or part of a query, as in this case).
To answer your question about whether unsafe input could do damage, I can think of two possibilities:
The SELECT passed as an argument to the procedure could be designed to be very resource-intensive. For example a query with a large cartesian join:
SELECT * FROM AnyTable
CROSS JOIN AnyTable
CROSS JOIN AnyTable
CROSS JOIN AnyTable
CROSS JOIN AnyTable
ORDER BY 1;
Generates a result set of a few trillion rows, and attempts to sort it. That could be a denial-of-service attack.
The SELECT includes calling a stored function that modifies data.
SELECT SomeFunctionThatDeletes() FROM AnyTable;
Whether your procedure is unsafe depends on whether the calling code allows untrusted input to become the argument to your procedure.
Suppose you had some safeguard like this (written in PHP, but the same applies to any language):
$user_option = $_GET['option'];
$queries = [
1 => 'select * from table where lastModifiedTime > :p_asof and <conditions>',
2 => 'select * from table where lastModifiedTime < :p_asof and <conditions>'
];
$stmt = $db->prepare('call getByFilter(?)');
$stmt->execute([$queries[$user_option]);
Now the user can pick with an input of '1' or '2' which query to run, but they have to pick one or the other of these hard-coded queries. They can't do anything mischievous like change the SQL query that is run.
That's a type of whitelisting. In other words, your application code must limit the choices so that the user's input can pick only safe choices. The user input should never be allowed to be executed as SQL directly without some kind of whitelisting like this.
You could also implement the whitelisting inside your procedure.

Oracle: Pure PL/SQL data extraction and anonymization using temporary tables, read-only permissions

I am trying to create a PL/SQL script that extracts a root "object" together with all children and other relevant information from an oracle production database. The purpose is to create a set of test-data to recreate issues that are encountered in production. Due to data protection laws the data needs to be anonymized when extracted - object names, certain types of id's, and monetary amounts need to be replaced.
I was trying to create one or more temporary translation tables, which would contain both the original values and anonymized versions. Then I would join the real data with the translation tables and output the anonymized values wherever required.
DECLARE
rootId integer := 123456;
TYPE anonTableRow IS RECORD
(
id NUMBER,
fieldC NUMBER,
anonymizedFieldC NUMBER
);
TYPE anonTable IS TABLE OF anonTableRow;
anonObject anonTable;
BEGIN
FOR cursor_row IN
(
select
id,
fieldC,
1234 -- Here I would create anonymized values based on rowNum or something similar
from
prodTable
where id = rootId
)
LOOP
i := i + 1;
anonObject(i) := cursor_row;
END LOOP;
FOR cursor_row IN
(
select
prod_table.id,
prod_table.fieldB,
temp_table.anonymizedFieldC fieldC,
prod_table.fieldD
from
prod_table
inner join table(temp_table) on prod_table.id = temp_table.id
where prod_table.id = 123456789
)
LOOP
dbms_output.put_line('INSERT INTO prod_table VALUES (' || cursor_row.id || ', ' || cursor_row.fieldB || ', ' || cursor_row.fieldC || ', , ' || cursor_row.fieldD);
END LOOP;
END;
/
However I ran into several problems with this approach - it seems to be near impossible to join oracle PL/SQL tables with real database tables. My access to the production database is severely restricted, so I cannot create global temporary tables, declare types outside PL/SQL or anything of that sort.
My attempt to declare my own PL/SQL types failed with the problems mentioned in this question - the solution does not work for me because of the limited permissions.
Is there a pure PL/SQL way that does not require fancy permissions to achieve something like the above?
Please Note: The above code example is simplified a lot and would not really require a separate translation table - in reality I need access to the original and translated values in several different queries, so I would prefer not having to "recalculate" translations everywhere.
If your data is properly normalized, then I guess this should only be necessary for internal IDs (not sure why you need to translate them though).
The following code should work for you, keeping the mappings in Associative Arrays:
DECLARE
TYPE t_number_mapping IS TABLE OF PLS_INTEGER INDEX BY PLS_INTEGER;
mapping_field_c t_number_mapping;
BEGIN
-- Prepare mapping
FOR cur IN (
SELECT 101 AS field_c FROM dual UNION ALL SELECT 102 FROM dual -- test-data
) LOOP
mapping_field_c(cur.field_c) := mapping_field_c.COUNT; -- first entry mapped to 1
END LOOP;
-- Use mapping
FOR cur IN (
SELECT 101 AS field_c FROM dual UNION ALL SELECT 102 FROM dual -- test-data
) LOOP
-- You can use the mapping when generating the `INSERT` statement
dbms_output.put_line( cur.field_c || ' mapped to ' || mapping_field_c(cur.field_c) );
END LOOP;
END;
Output:
101 mapped to 1
102 mapped to 2
If this isn't a permanent piece of production code, how about "borrowing" an existing collection type - e.g. one define in SYS that you can access.
Using this script from your schema you can generate a SQL Plus script to describe all SYS-owned types:
select 'desc ' || type_name from all_types
where typecode = 'COLLECTION'
and owner = 'SYS';
Running the resulting script will show you the structure of all the ones you can access. This one looks potentially suitable for example:
SQL> desc KU$_PARAMVALUES1010
KU$_PARAMVALUES1010 TABLE OF SYS.KU$_PARAMVALUE1010
Name Null? Type
----------------------------------------- -------- ----------------------------
PARAM_NAME VARCHAR2(30)
PARAM_OP VARCHAR2(30)
PARAM_TYPE VARCHAR2(30)
PARAM_LENGTH NUMBER
PARAM_VALUE_N NUMBER
PARAM_VALUE_T VARCHAR2(4000)
Of course, you can't guarantee that type will still exist or be the same or be accessible to you after a database upgrade, hence my caveat at the start.
More generic way to achieve this goal.
In my example i'm using xquery flwor expressions and dbms_xmlstore. Knowledge about xquery is mandatory.
create table mask_user_objects as select * from user_objects where rownum <0;
declare
v_s_table varchar2(30) := 'USER_OBJECTS'; --uppercase!!!
v_d_table varchar2(30) := 'MASK_USER_OBJECTS'; --uppercase!!!
v_mask_columns xmltype := xmltype('<COLS><OBJECT_NAME>XXXX</OBJECT_NAME>
<DATA_OBJECT_ID>-1</DATA_OBJECT_ID>
<OBJECT_TYPE/>
</COLS>'); --uppercase!!!
insCtx DBMS_XMLSTORE.ctxType;
r NUMBER;
v_source_table xmltype;
v_cursor sys_refcursor;
begin
open v_cursor for 'select * from '||v_s_table||' where rownum <100 ';
v_source_table := xmltype(v_cursor);
close v_cursor;
-- Load source table into xmltype.
insCtx := DBMS_XMLSTORE.newContext(v_d_table); -- Get saved context
for rec in (
select tt.column_value from xmltable('
let $col := $anomyze/COLS
for $i in $doc/ROWSET/ROW
let $row := $i
return <ROWSET>
<ROW>
{
for $x in $row/*
return if(
exists($col/*[name() = $x/name()] )
) then element{$x/name()}{ $col/*[name() = $x/name()]/text() }
else element{$x/name()}{$x/text()}
}
</ROW>
</ROWSET>
'
passing v_source_table as "doc"
, v_mask_columns as "anomyze"
) tt) loop
null;
r := DBMS_XMLSTORE.insertXML(insCtx, rec.column_value);
end loop;
DBMS_XMLSTORE.closeContext(insCtx);
end;

ROWID in casted pl-sql collection

Is there any rowid like facility in a pl-sql collection? In my case, while I am using this collection in an sql query, I also need the sequence number as they are put in. I know modification is data struecture is a way, but I want to use the index of the collection. so what I am looking for is something like this:
TYPE t_List IS TABLE OF VARCHAR2(200);
and
declare
v_Data t_List := t_List('data 1'
,'data_2'
,'data3');
......
FOR Rec IN (SELECT Column_Value v
,ROWID r
FROM TABLE(CAST(v_data t_list)))
LOOP
Dbms_Output.Put_Line('at ' || Rec.r || ':' || Rec.v);
-- .... and other codes here
END LOOP;
The loop is not expected to be executed in sequence, but I want something built-in like ROWID that is like the index of the collection.
Only schema-level types can be used in SQl statement, even within a PL/SQL block. As you seem to suggest you already know, you can create your own object type that includes the 'sequence' ID:
CREATE TYPE t_object AS OBJECT (
id NUMBER,
data VARCHAR2(200)
)
/
And a collection of that type:
CREATE TYPE t_List IS TABLE OF t_object;
/
And then populate the ID as you build the list:
DECLARE
l_List t_List := t_List(t_object(1, 'data 1')
,t_object(2, 'data_2')
,t_object(3, 'data3'));
BEGIN
FOR Rec IN (SELECT id, data
FROM TABLE(l_list))
LOOP
Dbms_Output.Put_Line('at ' || Rec.id || ':' || Rec.data);
-- .... and other codes here
END LOOP;
END;
/
Without an object type you can use the ROWNUM pseudocolumn:
CREATE TYPE t_List IS TABLE OF VARCHAR2(200);
/
DECLARE
v_Data t_List := t_List('data 1'
,'data_2'
,'data3');
BEGIN
FOR Rec IN (SELECT Column_Value v
,ROWNUM r
FROM TABLE(v_data))
LOOP
Dbms_Output.Put_Line('at ' || Rec.r || ':' || Rec.v);
-- .... and other codes here
END LOOP;
END;
/
anonymous block completed
at 1:data 1
at 2:data_2
at 3:data3
As far as I'm aware that isn't guaranteed to preserve the original creation sequence. I think it almost certainly will at the moment, but perhaps isn't something you should rely on as always being true. (There is no order without an order by, but here you don't have anything you can order by without destroying your initial order...).
If you query a subset of the table - I'm not sure what you mean by "the loop is not expected to be executed in sequence" - you'd need to generate the ROWNUM in a subquery before filtering or it won't be consistent. You'd also need to generate the ROWNUM in a subquery if you're joining this to other, real, tables - I imagine you are, otherwise you could use a PL/SQL collection.
If indexing is important, use Associative Arrays(Also known as Index-by tables)
Refer his.

Resources