Erstellen einer dynamischen Oracle Where-Klausel

7

Ich arbeite an einer Anwendung, die eine dynamische Abfrage verwendet, um eine Select-Anweisung basierend auf Benutzereingaben auszuführen. Nachdem ich die Sicherheit mit DBAs besprochen habe, soll ich meine dynamische Select-Anweisung in eine gespeicherte Prozedur konvertieren.

Ich habe dynamisches SQL mit MSSQL erstellt, kann aber nicht herausfinden, wie es in Oracle SQL konvertiert werden kann.

CREATE PROCEDURE GetCustomer
@FirstN nvarchar(20) = NULL,
@LastN nvarchar(20) = NULL,
@CUserName nvarchar(10) = NULL, 
@CID nvarchar(15) = NULL as
DECLARE @sql nvarchar(4000),
SELECT @sql = 'C_FirstName, C_LastName, C_UserName, C_UserID ' + 
'FROM CUSTOMER ' +
'WHERE 1=1 ' +

IF @FirstN  IS NOT NULL
SELECT @sql = @sql + ' AND C_FirstName like @FirstN '
IF @LastN  IS NOT NULL 
SELECT @sql = @sql + ' AND C_LastName like @LastN '
IF @CUserName IS NOT NULL
SELECT @sql = @sql + ' AND C_UserName like @CUserName '
IF @CID IS NOT NULL 
SELECT @sql = @sql + ' AND C_UserID like @CID '
EXEC sp_executesql @sql, N'@C_FirstName nvarchar(20), @C_LastName nvarchar(20), @CUserName nvarchar(10), @CID nvarchar(15)',
                   @FirstN, @LastN, @CUserName, @CID

* Bitte beachten Sie, dass ich die SQL-Injection verhindern möchte. Ich möchte nicht nur einen String zusammenfügen

** Ich habe eine separate Klasse zum Erstellen dieser dynamischen Abfrage für meine Anwendung in .net erstellt. Ich habe fast 1000 Codezeilen, um alles zu verarbeiten und SQL-Injection zu verhindern, aber DBAs haben mir mitgeteilt, dass sie gespeicherte Prozeduren wünschen, damit sie Eingaben und steuern können Ausgabe.

Vladimir Oselsky
quelle
Warum verschwenden Sie 1000 Codezeilen, wenn Ihre gespeicherte SQL-Server-Prozedur gegen SQL-Injection immun ist? dba.stackexchange.com/questions/790/… ?
Bernd_k

Antworten:

7

Dies könnte Ihnen eine Idee geben:

create table Customer (
  c_firstname varchar2(50),
  c_lastname  varchar2(50),
  c_userid    varchar2(50)
);

insert into Customer values ('Micky' , 'Mouse', 'mm');
insert into Customer values ('Donald', 'Duck' , 'dd');
insert into Customer values ('Peter' , 'Pan'  , 'pp');

create or replace function GetCustomer(
  FirstN    varchar2 := null,
  LastN     varchar2 := null,
  CID       varchar2 := null
) return sys_refcursor
as
  stmt varchar2(4000);
  ret sys_refcursor;
begin
  stmt := 'select * from Customer where 1=1';

  if  FirstN is not null then
      stmt := stmt || ' and c_firstname like ''%' || FirstN || '%''';
  end if;

  if  LastN is not null then
      stmt := stmt || ' and c_lastname like ''%' || LastN  || '%''';
  end if;

  if  CID is not null then
      stmt := stmt || ' and c_userid like ''%' || CID || '%''';
  end if;

  dbms_output.put_line(stmt);

  open ret for stmt;
  return ret;
end;
/

Später in SQL * Plus:

set serveroutput on size 100000 format wrapped

declare
  c sys_refcursor;
  fn Customer.c_firstname%type;
  ln Customer.c_lastname %type;
  id Customer.c_userid   %type;
begin
  c := GetCustomer(LastN => 'u');

  fetch c into fn, ln, id;
  while  c%found loop
      dbms_output.put_line('First Name: ' || fn);
      dbms_output.put_line('Last Name:  ' || ln);
      dbms_output.put_line('user id:    ' || id);

      fetch c into fn, ln, id;
  end loop;

  close c;
end;
/

Bearbeiten : Der Kommentar ist richtig und die Prozedur unterliegt der SQL-Injection . Um dies zu verhindern, können Sie Bindungsvariablen wie in dieser geänderten Prozedur verwenden:

create or replace function GetCustomer(
  FirstN    varchar2 := null,
  LastN     varchar2 := null,
  CID       varchar2 := null
) return sys_refcursor
as
  stmt varchar2(4000);
  ret  sys_refcursor;

  type parameter_t is table of varchar2(50);
  parameters parameter_t := parameter_t();
begin
  stmt := 'select * from Customer where 1=1';

  if  FirstN is not null then
      parameters.extend;
      parameters(parameters.count) := '%' || FirstN || '%';
      stmt := stmt || ' and c_firstname like :' || parameters.count;
  end if;

  if  LastN is not null then
      parameters.extend;
      parameters(parameters.count) := '%' || LastN || '%';
      stmt := stmt || ' and c_lastname like :' || parameters.count;
  end if;

  if  CID is not null then
      parameters.extend;
      parameters(parameters.count) := '%' || CID || '%';
      stmt := stmt || ' and c_userid like :' || parameters.count;
  end if;


  if    parameters.count = 0 then
        open ret for stmt;
  elsif parameters.count = 1 then
        open ret for stmt using parameters(1);
  elsif parameters.count = 2 then
        open ret for stmt using parameters(1), parameters(2);
  elsif parameters.count = 3 then
        open ret for stmt using parameters(1), parameters(2), parameters(3);
  else  raise_application_error(-20800, 'Too many parameters');
  end   if;

  return ret;
end;
/

Beachten Sie, dass die select-Anweisung nun unabhängig von der Eingabe select ... from ... where 1=1 and col1 like :1 and col2 :2 ...offensichtlich viel sicherer wird.

René Nyffenegger
quelle
1
Wenn FirstN nicht null ist, ist stmt: = stmt || 'und c_firstname wie' || Erst ein; ende wenn; Ich kann sehen, dass Sie in Ihrem Beispiel die Zeichenfolge kombiniert haben? Woher wissen Sie, dass Benutzer die SQL-Injection nicht in den Parameter einfügen?
Vladimir Oselsky
é Ihre Lösung mit Bindevariablen sieht sehr vielversprechend aus.
Bernd_k
1
@Sauce Ich habe eine zweite Antwort hinzugefügt, um zu demonstrieren, warum Renés Code immun gegen SQL-Injektionen ist
bernd_k
6

Sie benötigen nicht unbedingt dynamisches SQL, nur weil bestimmte Bedingungen nicht zutreffen, wenn sie nicht vorhanden sind.

SELECT 
    C_FirstName, C_LastName, C_UserName, C_UserID 
FROM 
    CUSTOMER
WHERE 
    (FirstN IS NULL OR C_FirstName LIKE FirstN)
    AND (LastN IS NULL OR C_LastName LIKE LastN)
    AND (CUserName IS NULL OR C_UserName LIKE CUserName)
    AND (CID IS NULL OR C_UserID LIKE CID)

Das Platzieren dieses Codes in einer gespeicherten Prozedur in einem Paket ist eine hervorragende Idee.

Oracle bietet eine hervorragende Dokumentation, mit der Sie sich über gespeicherte Prozeduren und Pakete auf dem Laufenden halten können. Beginnen Sie möglicherweise mit dem Konzepthandbuch , um zu verstehen, wie Oracle funktioniert, und fahren Sie dann mit der SQL-Sprachreferenz und der PL / SQL-Sprachreferenz fort, um Informationen zu Ihrer aktuellen Aufgabe zu erhalten.

Leigh Riffel
quelle
Also, wenn ich deinen Code verstehe. Sie interessieren sich nur für Variablen, die nicht null sind, zum Beispiel, wenn ich FirstN und CID übergebe. Was ich ausführe, sieht folgendermaßen aus: SELECT C_FirstName, C_LastName, C_UserName, C_UserID FROM CUSTOMER WHERE C_FirstName LIKE FirstN UND C_UserID LIKE CID
Vladimir Oselsky
Was Sie ausführen, sieht identisch aus. Die verwendete Abfragelogik kümmert sich darum, ob die Variable verwendet wird, um Zeilen aus den Ergebnissen zu begrenzen.
Leigh Riffel
Was ist mit der Validierung der Variablen, um sicherzustellen, dass jemand keine SQL-Injection eingibt?
Vladimir Oselsky
Angenommen, Sie fügen dies in ein Paket ein. Es handelt sich um Embedded SQL und nicht um Dynamic SQL. Daher ist es nicht anfällig für SQL-Injection. Weitere Informationen finden Sie im Oracle-Whitepaper "So schreiben Sie SQL Injection Proof PL / SQL" unter oracle.com/technetwork/database/features/plsql/overview/…
Leigh Riffel am
Das Ignorieren einer kaseinsensitiven Zeichenfolge vergleicht dies mit einer korrekten Lösung. Die Leistung einzelner Abfragen ist nicht optimal. Bei Verwendung in einer gespeicherten Prozedur treten keine Probleme auf, wenn viele Abfragen mit unterschiedlichen Parametern ausgeführt werden. Es ist eine gute Basis für Leistungsvergleiche.
Bernd_k
1

Dies ist keine unabhängige Antwort, sondern eine zusätzliche Erklärung für den Code von René Nyffenegger unter Verwendung von Bindungsvariablen.

SaUce fragte, warum dieser Code immun gegen SQL-Injektion ist.

Hier ändere ich Renés Code, um die dynamische Anweisung nicht auszuführen, sondern anzuzeigen:

create or replace function GetCustomer(
  FirstN    varchar2 := null,
  LastN     varchar2 := null,
  CID       varchar2 := null
) return sys_refcursor
as
  stmt varchar2(4000);
  ret  sys_refcursor;

  type parameter_t is table of varchar2(50);
  parameters parameter_t := parameter_t();
begin
  stmt := 'select * from Customer where 1=1';

  if  FirstN is not null then
      parameters.extend;
      parameters(parameters.count) := '%' || FirstN || '%';
      stmt := stmt || ' and c_firstname like :' || parameters.count;
  end if;

  if  LastN is not null then
      parameters.extend;
      parameters(parameters.count) := '%' || LastN || '%';
      stmt := stmt || ' and c_lastname like :' || parameters.count;
  end if;

  if  CID is not null then
      parameters.extend;
      parameters(parameters.count) := '%' || CID || '%';
      stmt := stmt || ' and c_userid like :' || parameters.count;
  end if;


   OPEN ret for SELECT stmt FROM DUAL;


  return ret;
end;
/

Jetzt kann ich Anrufe wie versuchen

Var r refcursor
exec  GetCustomer(:r, 'Micky', '')
print r

Das Ergebnis ist:

Wählen Sie * vom Kunden, wobei 1 = 1 und FirstN wie folgt: 1

In Renés Code wird dies wie folgt ausgeführt:

select * from Customer where 1=1  and FirstN like :1 using 'Micky'

Sie sehen, es spielt keine Rolle, welcher Wert für FirstN angegeben wird. Die Bedeutung der Abfrage wird nie geändert.

Es gibt weitere Gründe für die Verwendung der Variablenbindung, die für Entwickler mit SQL Server-Hintergrund schwer zu verstehen sind. Sie hängen davon ab, wie Oracle vorkompilierte Ausführungspläne im gemeinsam genutzten Pool speichert. Wenn keine Bindungsvariablen verwendet werden, ergeben sich unterschiedliche Anweisungen und Ausführungspläne, während bei Verwendung von Bindungsvariablen ein einzelner Ausführungsplan verwendet wird.

bernd_k
quelle
0

Für Ihre gespeicherte Prozedur würde die beste Migration zu Oracle aussehen

CREATE or replace PROCEDURE GetCustomer 
    p_FirstN nvarchar2 := NULL, 
    p_LastN nvarchar2 := NULL, 
    p_CUserName nvarchar2 := NULL, 
    p_CID nvarchar2 := NULL, 
    MyRefCursor IN OUT typRefCursor
as 
begin   

    IF p_FirstN IS NULL then
        if p_p_LastN is null then
            if p_CUserName is null then
                if  p_CID is null then
                    Open MyRefCursor for Select C_FirstName, C_LastName, C_UserName, C_UserID FROM CUSTOMER; 
                else
                    Open MyRefCursor for Select C_FirstName, C_LastName, C_UserName, C_UserID FROM CUSTOMER WHERE upper(C_UserID) like upper(p_CID) ; 
                end;        
            else
                if  p_CID is null then
                    Open MyRefCursor for Select C_FirstName, C_LastName, C_UserName, C_UserID FROM CUSTOMER WHERE UPPER(C_UserName) like UPPER(p_CUserName); 
                else
                    Open MyRefCursor for Select C_FirstName, C_LastName, C_UserName, C_UserID FROM CUSTOMER WHERE upper(C_UserID) like upper(p_CID) and UPPER(C_UserName) like UPPER(p_CUserName); 
                end;        
            end if;
        else
            if p_CUserName is null then
                if  p_CID is null then
                    ...
                else
                    ...
                end;        
            else
                if  p_CID is null then
                    ...
                else
                    ...
                end;        
            end if;
        end if;
    else
        if p_p_LastN is null then
            if p_CUserName is null then
                if  p_CID is null then
                    ...
                else
                    ...
                end;        
            else
                if  p_CID is null then
                    ...
                else
                    ...
                end;        
            end if;
        else
            if p_CUserName is null then
                if  p_CID is null then
                    ...
                else
                    ...
                end;        
            else
                if  p_CID is null then
                    ...
                else
                    ...
                end;        
            end if;
        end if;
    end if; 

end;
/

OK, es ist spät und ich bin ein bisschen faul, aber die restlichen 12 Fälle auszufüllen ist einfach.

Die Nichtverwendung von dynamischem SQL hat einige Vorteile:

  1. Sie können Ihre Syntax beim Kompilieren überprüfen
  2. Sie müssen nicht mit Kontexten herumspielen

Denken Sie nicht, weil es für einen Menschen langweilig aussieht, dass es für einen Computer schlecht ist (insbesondere wenn Oracle ausgeführt wird).

Fügen Sie jedoch keine weiteren Parameter hinzu, um mich zu zwingen, eine Lösung mit dynamischem SQL anzuzeigen. Vermeiden Sie stattdessen verrückte Designs, die solche Lösungen erfordern.

bernd_k
quelle
Nun, das ist gut, wenn Sie nur 3 Parameter durchlaufen müssen. Was ist mit 10 verschiedenen Parametern? Ich weiß, dass ich dies einfach für etwas bauen kann, für das Äther 1 oder 2 Parameter hat, aber es wird nicht einfach sein, es zu skalieren, um verschiedene Parameter zu akzeptieren und den Inhalt zu ändern. Sobald ich 1 Prozedur beendet habe, muss ich ungefähr 6 weitere für die Eingabe mehrerer Parameter schreiben.
Vladimir Oselsky