99

I want to pass a table name as a parameter in a Postgres function. I tried this code:

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
    BEGIN
    IF EXISTS (select * from quote_ident($1) where quote_ident($1).id=1) THEN
     return 1;
    END IF;
    return 0;
    END;
$$ LANGUAGE plpgsql;

select some_f('table_name');

And I got this:

ERROR:  syntax error at or near "."
LINE 4: ...elect * from quote_ident($1) where quote_ident($1).id=1)...
                                                             ^

********** Error **********

ERROR: syntax error at or near "."

And here is the error I got when changed to this select * from quote_ident($1) tab where tab.id=1:

ERROR:  column tab.id does not exist
LINE 1: ...T EXISTS (select * from quote_ident($1) tab where tab.id...

Probably, quote_ident($1) works, because without the where quote_ident($1).id=1 part I get 1, which means something is selected. Why may the first quote_ident($1) work and the second one not at the same time? And how could this be solved?

4

8 に答える 8

148

これはさらに単純化して改善できます。

CREATE OR REPLACE FUNCTION some_f(_tbl regclass, OUT result integer)
    LANGUAGE plpgsql AS
$func$
BEGIN
   EXECUTE format('SELECT (EXISTS (SELECT FROM %s WHERE id = 1))::int', _tbl)
   INTO result;
END
$func$;

スキーマ修飾名で呼び出します (以下を参照):

SELECT some_f('myschema.mytable');  -- would fail with quote_ident()

または:

SELECT some_f('"my very uncommon table name"');

主なポイント

OUTパラメータを使用して関数を単純化します。動的 SQL の結果を直接選択して実行できます。追加の変数やコードは必要ありません。

EXISTSあなたが望むことを正確に行います。true行が存在するかどうかを取得しfalseます。これにはさまざまな方法がありますが、EXISTS通常は最も効率的です。

あなたは整数booleanを返したいようですので、結果を からEXISTSにキャストしintegerます。これにより、まさにあなたが持っていたものが得られます。代わりにブール値を返します。

オブジェクト識別子型regclassを の入力型として使用します_tbl。それはすべてを行うか、quote_ident(_tbl)またはformat('%I', _tbl)行うだろうが、より良い理由は次のとおりです。

  • .. SQL インジェクションも防止します。

  • .. テーブル名が無効である/存在しない/現在のユーザーに表示されない場合、すぐに失敗し、より適切に失敗します。(パラメーターは、既存のテーブルregclassにのみ適用されます。)

  • ..あいまいさを解決できないため、単純なquote_ident(_tbl)orが失敗するスキーマ修飾テーブル名で動作します。format(%I)スキーマ名とテーブル名を別々に渡してエスケープする必要があります。

明らかに、既存のテーブルでのみ機能します。

format()構文を簡素化するため(および使用方法を示すため)、私はまだ を使用していますが、%s代わりに. を使用してい%Iます。通常、クエリはより複雑であるためformat()、より役立ちます。簡単な例では、連結することもできます。

EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'

リストidにテーブルが 1 つしかない場合は、列をテーブル修飾する必要はありません。FROMこの例ではあいまいさがありません。(動的) 内部の SQL コマンドにEXECUTE別のスコープがあり、関数変数またはパラメーターはそこには表示されません - 関数本体のプレーンな SQL コマンドとは対照的です。

動的 SQL のユーザー入力を常に適切にエスケープする理由は次のとおりです。

db<>fiddle here SQL インジェクションのデモンストレーション
Old sqlfiddle

于 2012-05-22T23:07:21.583 に答える
13

可能であれば、これを行わないでください。

それが答えです。これはアンチパターンです。クライアントがデータを取得するテーブルを知っている場合、SELECT FROM ThatTable. これが必要な方法でデータベースが設計されている場合、それは最適に設計されていないように見えます。データ アクセス層が値がテーブルに存在するかどうかを知る必要がある場合、そのコードで SQL を構成するのは簡単であり、このコードをデータベースにプッシュするのは適切ではありません。

私には、これは、希望する階数を入力できる装置をエレベーター内に設置するようなものに思えます。Go ボタンを押すと、機械の針が目的のフロアの正しいボタンに移動し、それを押します。これにより、多くの潜在的な問題が発生します。

注意: ここでは嘲笑の意図はありません。私のばかげたエレベーターの例は、この手法の問題点を簡潔に指摘するための *私が想像できる最高のデバイス* でした。これは無用な間接層を追加し、テーブル名の選択を呼び出し元の空間から (堅牢でよく理解された DSL、SQL を使用して) から、あいまい/奇妙なサーバー側 SQL コードを使用するハイブリッドに移動します。

このようにクエリ構築ロジックを動的 SQL に移動することで責任を分割すると、コードが理解しにくくなります。これは、エラーの可能性をはらむカスタム コードの名目で、標準的で信頼性の高い規則 (SQL クエリが何を選択するか) に違反しています。

このアプローチの潜在的な問題のいくつかに関する詳細なポイントを次に示します。

  • 動的 SQL は、フロント エンド コードまたはバック エンド コードだけでは認識しにくい SQL インジェクションの可能性を提供します (これを確認するには、それらを一緒に検査する必要があります)。

  • ストアド プロシージャと関数は、SP/関数の所有者には権限がありますが、呼び出し元には権限がないリソースにアクセスできます。私が理解している限りでは、特別な注意を払わなくても、デフォルトでは、動的 SQL を生成して実行するコードを使用すると、データベースは呼び出し元の権限で動的 SQL を実行します。これは、特権オブジェクトをまったく使用できないか、すべてのクライアントに対してそれらをオープンにする必要があることを意味し、特権データへの潜在的な攻撃の対象領域を増やします。作成時に SP/関数を常に特定のユーザーとして実行するように設定すると (SQL Server ではEXECUTE AS)、その問題は解決する可能性がありますが、事態はより複雑になります。これにより、動的 SQL が非常に魅力的な攻撃ベクトルになるため、前述の SQL インジェクションのリスクが悪化します。

  • アプリケーション コードを変更したりバグを修正したりするために、開発者がアプリケーション コードが何を行っているかを理解しなければならない場合、実行されている正確な SQL クエリを取得するのは非常に困難です。SQL プロファイラーを使用できますが、これには特別な特権が必要であり、運用システムのパフォーマンスに悪影響を及ぼす可能性があります。実行されたクエリは SP によってログに記録できますが、これにより複雑さが増し、疑わしい利点 (新しいテーブルの収容、古いデータのパージなどが必要) が発生し、まったくわかりません。実際、一部のアプリケーションは、開発者がデータベース資格情報を持たないように設計されているため、送信されたクエリを実際に見ることはほとんど不可能になっています。

  • 存在しないテーブルを選択しようとした場合など、エラーが発生すると、データベースから「無効なオブジェクト名」というメッセージが表示されます。これは、SQL をバックエンドで構成する場合でもデータベースで構成する場合でもまったく同じように発生しますが、違いは、システムのトラブルシューティングを行おうとしている一部の貧弱な開発者は、SQL が作成されている場所の下のさらに別の洞窟に 1 レベル深く潜り込まなければならないことです。問題が存在する場合、問題が何であるかを理解しようとする驚異的な手順を掘り下げます。ログには「GetWidget のエラー」は表示されず、「OneProcedureToRuleThemAllRunner のエラー」が表示されます。この抽象化は、一般にシステムを悪化させます。

パラメータに基づいてテーブル名を切り替える疑似 C# の例:

string sql = $"SELECT * FROM {EscapeSqlIdentifier(tableName)};"
results = connection.Execute(sql);

これは考えられるすべての問題を排除するわけではありませんが、他の手法で説明した欠陥はこの例にはありません。

于 2012-07-11T00:20:02.803 に答える
10

plpgsql コード内では、テーブル名または列が変数に由来するクエリには、EXECUTEステートメントを使用する必要があります。また、が動的に生成さIF EXISTS (<query>)れる場合、この構成は許可されません。query

両方の問題が修正された関数は次のとおりです。

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
DECLARE
 v int;
BEGIN
      EXECUTE 'select 1 FROM ' || quote_ident(param) || ' WHERE '
            || quote_ident(param) || '.id = 1' INTO v;
      IF v THEN return 1; ELSE return 0; END IF;
END;
$$ LANGUAGE plpgsql;
于 2012-05-22T18:49:52.880 に答える
4

最初のものは、あなたが意味する意味で実際には「機能」しません。エラーを生成しない限り、機能します。

を試してみるSELECT * FROM quote_ident('table_that_does_not_exist');と、関数が1を返す理由がわかります。selectは、quote_ident1つの行(変数$1またはこの特定の場合table_that_does_not_exist)を持つ1つの列(名前付き)を持つテーブルを返します。

あなたがしたいことは動的SQLを必要とします、それは実際にquote_*関数が使われることを意図されている場所です。

于 2012-05-22T16:35:02.570 に答える