2

I have a variable which is an array of string. I want to pass all the values of the variable, concatenating all elements of it into a single string.

But I'm not sure whether this poses risks of SQL injection. My code:

private string concatenateStrings(string[] sa)
{
    StringBuilder sb = new StringBuilder();

    foreach (string s in sa)
    {
        if (sb.Length > 0)
        {
            sb.Append(",");
        }
        sb.Append("'");
        sb.Append(s);
        sb.Append("'");
    }
    return sb.ToString();
}

public void UpdateClaimSts(string[] ids)
{
    string query = @"UPDATE MYTABLE
                    SET STATUS = 'X'
                    WHERE TABLEID in (" + concatenateStrings(ids) + ")";

    OracleCommand dbCommand = (OracleCommand)this.Database.GetSqlStringCommand(query) as OracleCommand;
    this.Database.ExecuteNonQuery(dbCommand, this.Transaction);
}

I tried changing the query to use parameterized queries:

string query = @"UPDATE MYTABLE
                SET STATUS = 'X'
                WHERE TABLEID in (:ids)";

OracleCommand dbCommand = (OracleCommand)this.Database.GetSqlStringCommand(query) as OracleCommand;

dbCommand.Parameters.Add(":ids", OracleType.VarChar).Value = concatenateStrings(ids);
this.Database.ExecuteNonQuery(dbCommand, this.Transaction);

But it does not work. Any ideas?

rcs
  • 6,713
  • 12
  • 53
  • 75
  • dbCommand.Parameters.AddWithValue("@mydata", ids) – Trish Siquian Sep 12 '18 at 07:17
  • 1
    In your first example, if my first `id` is `1');DROP TABLE MYTABLE;--` (for example) then you'll have some trouble – Rafalon Sep 12 '18 at 07:25
  • @TrishSiquian It does not work. Basically what you do is the same of what I try to do in my second example. – rcs Sep 12 '18 at 07:29
  • @Rafalon That's why I ask for suggestion on how to prevent SQL injection. – rcs Sep 12 '18 at 07:30
  • @rcs Yes, I was just answering to this: "*But I'm not sure whether this poses risks of SQL injection*" – Rafalon Sep 12 '18 at 07:31
  • Possible duplicate of [Pass Array Parameter in SqlCommand](https://stackoverflow.com/questions/2377506/pass-array-parameter-in-sqlcommand) – Cid Sep 12 '18 at 07:33
  • Unrelated, but your `concatenateStrings` could just as well `return "'"+String.Join("','",sa)+"'";` :) – Rafalon Sep 12 '18 at 07:33
  • Are your ids really strings? if they are int then you can pass int[] ids and that would do it. Same with GUIDs. But if it's really strings, then dunno. – Kajetan Kazimierczak Sep 12 '18 at 07:41
  • You can check if all the ids (items of `string[] sa`) are in fact integers: `Regex.IsMatch(item @"^\-?[0-9]+$")` – Dmitry Bychenko Sep 12 '18 at 07:45
  • 2
    @Rafalon Unlike some databases, Oracle does not allow two commands in a single statement so that type of SQL injection attack will throw an error. There are other methods of attack that would work like `1) OR 1=1` to bypass the filter and update every row or `1) AND EXISTS( SELECT 1 FROM password_table WHERE user = 'Admin' AND hash = '0123456' )` to look for the existence of other tables and sensitive data. – MT0 Sep 12 '18 at 08:46
  • Just a note, this approach will fail if you have more than 4000 elements. I don't know whether this is relevant in your case. – Wernfried Domscheit Sep 12 '18 at 08:53
  • It's ok, I don't have that many elements – rcs Sep 12 '18 at 09:04
  • @MT0 That's good to know, thanks. I actually didn't see the `oracle` tag, but didn't know that either – Rafalon Sep 12 '18 at 11:22

3 Answers3

3

Create a PL/SQL procedure (inside a PL/SQL Package) like this:

TYPE TArrayOfVarchar2 IS TABLE OF MYTABLE.TABLEID%TYPE INDEX BY PLS_INTEGER;

PROCEDURE UPDATE_MYTABLE(TABLEIDs IN TArrayOfVarchar2) IS
BEGIN

    FORALL i IN INDICES OF TABLEIDs
    UPDATE MYTABLE SET STATUS = 'X' WHERE TABLEID = TABLEIDs(i);

END;

and make a call like this:

using (OracleCommand cmd = new OracleCommand("BEGIN UPDATE_MYTABLE(:tableId); END;"), con))
{
  cmd.CommandType = CommandType.Text;
  // or
  // OracleCommand cmd = new OracleCommand("UPDATE_MYTABLE"), con);
  // cmd.CommandType = CommandType.StoredProcedure;
  var par = cmd.Parameters.Add("tableId", OracleDbType.Varchar2, ParameterDirection.Input);
  par.CollectionType = OracleCollectionType.PLSQLAssociativeArray;
  par.Value = sa;
  par.Size = sa.Length;

  cmd.ExecuteNonQuery();
}
Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
Wernfried Domscheit
  • 54,457
  • 9
  • 76
  • 110
  • Unfortunately I may not be able to create the procedure in the database for some reason, and hence prefers a solution that can be done in C#. – rcs Sep 12 '18 at 08:38
1

As a quick and partial (we assume TABLEID field be of type NUMBER) solution you can verify that each item in sa is a valid integer:

private string concatenateStrings(string[] sa) {
   return string.Join(", ", sa
     .Where(item => Regex.IsMatch(item, @"^\-?[0-9]+$"))); 
} 

public void UpdateClaimSts(string[] ids) {
  string query = string.Format(
    @"UPDATE MYTABLE
         SET STATUS = 'X'
       WHERE TABLEID IN ({0})", concatenateStrings(ids));
      ...

In general case, you can try using bind variables (please, notice plural: we have to create many of them):

public void UpdateClaimSts(string[] ids) {  
  // :id_0, :id_1, ..., :id_N   
  string bindVariables = string.Join(", ", ids
    .Select((id, index) => ":id_" + index.ToString()));

  string query = string.Format(
    @"UPDATE MYTABLE
         SET STATUS = 'X'
       WHERE TABLEID IN ({0})", bindVariables);

  // Do not forget to wrap IDisposable into "using"
  using (OracleCommand dbCommand = ...) {
    ...
    // Each item of the ids should be assigned to its bind variable
    for (int i = 0; i < ids.Length; ++i)
      dbCommand.Parameters.Add(":id_" + i.ToString(), OracleType.VarChar).Value = ids[i];

   ...
Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
  • "you can verify that each item in sa is a valid integer" -- If everything is supposed to be an integer, the OP can just update the parameter to `int[]` instead of `string[]` and avoid the regex. Your second suggestion looks good to me though. –  Sep 12 '18 at 07:57
  • @hvd: not necessary - RDMS `Number` and .Net `int` (`long`) can well be quite different types (e.g. `Number(20)` exceeds `long`), that's why `int[]` instead of `string[]` is a dangerous desicion. However we often use `Number(N)` as a key and in this case my first partial solution will work – Dmitry Bychenko Sep 12 '18 at 08:02
  • Read `decimal[]` instead of `int[]`, then. My point was that your assumption that everything must be an integer is just that, an assumption. There is nothing preventing `TABLEID` from being a text column, and the fact that the OP *didn't* use `int[]` or `long[]` or `decimal[]` is at least a weak indication that it's not an integer column. –  Sep 12 '18 at 08:05
  • Is it possible if you do not use interpolated string? So that it can support legacy .net framework too ? – rcs Sep 12 '18 at 08:48
  • @rcs: Sure, interpolated strings can be changed into their `string.Format` equivalents – Dmitry Bychenko Sep 12 '18 at 08:51
0

C# has a OracleCollectionType.PLSQLAssociativeArray type for passing arrays to a PL/SQL Associative Array data type but this cannot be used in SQL queries as it is only a PL/SQL data structure.

It, unfortunately, does not support passing an array to an SQL Collection data type (which could be used in an SQL query).

One work around for this is to ask your DBA to create a simple function to convert a PL/SQL Associative Array to an SQL collection and then use this as an intermediate step in the query:

CREATE TYPE varchar2s_array_type IS TABLE OF VARCHAR2(100)
/

CREATE PACKAGE utils IS
  TYPE varchar2s_assoc_array_type IS TABLE OF VARCHAR2(100) INDEX BY PLS_INTEGER;

  FUNCTION assoc_array_to_collection(
    p_assoc_array IN varchar2s_assoc_array_type
  ) RETURN varchar2s_array_type DETERMINISTIC;
END;
/

CREATE PACKAGE BODY utils IS
  FUNCTION assoc_array_to_collection(
    p_assoc_array IN varchar2s_assoc_array_type
  ) RETURN varchar2s_array_type DETERMINISTIC
  IS
    p_array varchar2s_array_type := varchar2s_array_type();
    i PLS_INTEGER;
  BEGIN
    IF p_assoc_array IS NOT NULL THEN
      i := p_assoc_array.FIRST;
      LOOP
        EXIT WHEN i IS NULL;
        p_array.EXTEND();
        p_array(p_array.COUNT) := p_assoc_array(i);
        i := p_assoc_array.NEXT(i);
      END LOOP;
    END IF;
    RETURN p_array;
  END;
END;
/

Then you can change your code to use MEMBER OF rather than IN within the SQL statement:

UPDATE MYTABLE
SET    STATUS = 'X'
WHERE  TABLEID MEMBER OF utils.assoc_array_to_collection(:ids)

And bind the parameter using something like (I'm not a C# user so this is just to give you a general idea of the method, even if the syntax is not entirely correct):

var par = cmd.Parameters.Add(":ids", OracleDbType.Varchar2, ParameterDirection.Input);
par.CollectionType = OracleCollectionType.PLSQLAssociativeArray;
par.Value = ids;
par.Size = ids.Length;
cmd.ExecuteQuery();

Then you can reuse the generic function across many queries.

MT0
  • 143,790
  • 11
  • 59
  • 117