AWS WAF での SQL インジェクション対策と誤検知への対応方法

記事タイトルとURLをコピーする

環境

  • ユーザーが HTML フォームに入力した情報を、API Gateway を通じて Lambda 関数に送信し、その情報を Aurora データベースに保存します。
  • API Gateway には AWS WAF を設定しており、AWS のマネージドルールを使って SQL インジェクション攻撃からシステムを守っています。
  • AWS WAF のログは、できるだけリアルタイムで確認できるように CloudWatch Logs に記録しています。
  • さらに、Lambda 関数では、Prepared Statements を使って SQL 文とユーザーの入力を別々に扱うことで、SQL インジェクションを防いでいます。

・WEB サイトイメージ:

・Prepared Statements:java の例

String name = (String) event.get("name");
PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
pstmt.setString(1, name);
ResultSet rs = pstmt.executeQuery();

・Prepared Statements:Python の例

name = event['name']
sql = "SELECT * FROM users WHERE name = %s"
cur.execute(sql, name)

起きた問題

稀に、正しいリクエストでもSQLインジェクションとして誤ってブロックされることがあります。
ユーザー様からの問い合わせがあり、調査をすることにしました。

調査

CloudWatch の「ログのインサイト」機能を使って、ブロックしたログを確認してみましょう。

最初にシンプルなクエリを実行します。

fields @timestamp, @message
| limit 200

ある 1 つのログからブロックされている部分を抜粋します。

項目
01 ruleGroupList.0.ruleGroupId AWS#AWSManagedRulesSQLiRuleSet
02 ruleGroupList.0.terminatingRule.action BLOCK
03 ruleGroupList.0.terminatingRule.ruleId SQLi_BODY
04 terminatingRuleId AWS-AWSManagedRulesSQLiRuleSet
05 terminatingRuleMatchDetails.0.conditionType SQL_INJECTION
06 terminatingRuleMatchDetails.0.location BODY
07 terminatingRuleMatchDetails.0.matchedData.0 message=
08 terminatingRuleMatchDetails.0.matchedData.1 AND
09 terminatingRuleMatchDetails.0.matchedData.2 1
10 terminatingRuleMatchDetails.0.matchedData.3 =
11 terminatingRuleMatchDetails.0.matchedData.4 CONVERT
12 terminatingRuleMatchDetails.0.sensitivityLevel LOW
13 terminatingRuleType MANAGED_RULE_GROUP

4 行目の terminatingRuleId という項目は、リクエストをブロックしたルールを示しています。
7〜10 行目のterminatingRuleMatchDetails.0.matchedData.X は、ブロックする原因となった文字列を示しています。
リクエストのBODYに「message」「=」「1」「or」「=」という文字列が含まれているため、リクエストがブロックされています。
これらの文字列の1つ1つが問題ではなく、4つの文字列が組み合わさっていることでブロックされることもあります。

参考:ウェブACLトラフィックのログフィールド - AWS WAF、 AWS Firewall Manager、および AWS Shield Advanced

一致ルールでは、複数の検査基準の一致が必要になる場合があるため、これらの一致の詳細は、一致基準の配列として提供されます。

文字列の組み合わせのみを表示させてみます。

fields @timestamp, terminatingRuleMatchDetails.0.matchedData.0, terminatingRuleMatchDetails.0.matchedData.1, terminatingRuleMatchDetails.0.matchedData.2, terminatingRuleMatchDetails.0.matchedData.3, terminatingRuleMatchDetails.0.matchedData.4
| filter action = "BLOCK"
| filter terminatingRuleId = "AWS-AWSManagedRulesSQLiRuleSet"
| filter ruleGroupList.0.terminatingRule.ruleId = "SQLi_BODY"
| limit 200

検知されているパターンがなんとなくわかります。 httpRequest.uri という項目もあるため、パス毎に検知されているパターンを確認することもできます。

fields @timestamp, httpRequest.uri, terminatingRuleMatchDetails.0.matchedData.0, terminatingRuleMatchDetails.0.matchedData.1, terminatingRuleMatchDetails.0.matchedData.2, terminatingRuleMatchDetails.0.matchedData.3, terminatingRuleMatchDetails.0.matchedData.4
| filter action = "BLOCK"
| filter terminatingRuleId = "AWS-AWSManagedRulesSQLiRuleSet"
| filter ruleGroupList.0.terminatingRule.ruleId = "SQLi_BODY"
| limit 200

パスごとに、検知されているパターンを出力してみました。

AWS マネジメントコンソールでは、結果を JSON や CSV 形式で出力できます。
そのため、ファイルに出力してから編集することで、重複しているパターンを取り除くこともできます。

CSVを編集して、Markdown形式の表に変換してみました。

URI 検知文字列1 検知文字列2 検知文字列3 検知文字列4 検知文字列5
/test message = OR 1 =
/test message = UNION SELECT username
/test message= AND ( SELECT COUNT
/test message= AND 1 = CONVERT
/test message= UNION SELECT username FROM
/test/another message= AND 1 = CONVERT

リクエストのBODYに「=」「&」「users」などを含むと検知されやすい

代表的な SQL インジェクションには、以下のようなものがあります。

' OR '1'='1

これは、「 1 が 1 である」という常に真である条件を利用して、テーブルのすべてのデータを取得しようとする攻撃です。

' UNION SELECT username, password FROM users --

usersテーブルからusernamepasswordの列を選択するSQLクエリです。攻撃者はこれによって、ユーザー名とパスワードの情報を不正に取得しようとします。

上の例のような攻撃があるため、「=」や「users」といった文字列が含まれると、攻撃として検知されやすくなります。同じように、「&」や「-」などの条件式やコメントに使われる記号も、検知されやすい傾向があります。
これらの文字列が複数組み合わさると、特に検知されやすくなります。
select や open といった SQL 文で使われる文字列が組み合わさる場合も、検知されやすくなります。

Prepared Statement を利用する際に気を付けること

Prepared Statement を利用すると SQL 文とユーザーの入力を別々に扱うことで、SQL インジェクションを防ぐことができます。
利用を推奨される機能です。
上に挙げたSQLインジェクションの例は、Prepared Statementを利用していない場合を想定しています。SQLインジェクション対策のルールも、Prepared Statementが使われていない状況を考慮して作られていると思われます。
Prepared Statementを利用するプログラム内で、=& を使用することは一般的です。
そのため、Prepared Statementを使用する際には、usersusernamepasswordpasswdといった変数名は避けた方が良いと考えられます。これらを使うと、他の要素と組み合わさって正しいリクエストでもSQLインジェクションとして誤ってブロックされる可能性があるためです。
また、=& を使用しないようにするために、データをJSON形式でPOSTするのも良い方法だと思います。

既に運用中の環境においてプログラムの改修が困難な場合

スコープダウンステートメント

スコープダウンステートメントを使うと、「特定のURIだけを検査しない」といった設定をルールセットにすることができます。これにより、SQLインジェクションのルールセットをWEBサイト全体で無効にするよりも、より細かくコントロールできます。例えば、特定のページだけでルールを緩めることができるので、全体的なセキュリティを保ちながら柔軟に対応できます。

クラウド型 WAF の利用

もし、ログから原因を特定したり、スコープダウンステートメントを作成するのに時間がかけられない場合は、クラウド型WAFの利用を検討してみてください。弊社では、Cyber Security Cloud社のWafCharmを取り扱っていますので、お気軽にお問い合わせください。

24時間365日のサポートが付いているので、誤検知をはじめ万が一のトラブル発生時でも、原因特定やルール調整などはWafCharmサポートが対応するので安心です。 また、サポートは日本語のため言葉の壁の心配もありません。状況に応じて、メールか電話のどちらか最適な方法でサポートを受けられます。

CloudWatch Logs Insight のクエリに関する補足 2024/11/11

CloudWatch Logs Insights では、parsepattern 構文を使って、ログデータのパターンを集計することができます。
以下はその具体例です。

fields @timestamp
| filter terminatingRuleId = "AWS-AWSManagedRulesSQLiRuleSet"
| filter ruleGroupList.0.terminatingRule.ruleId = "SQLi_BODY"
| parse '"matchedData":[*]' as matched
| pattern matched
| limit 200

parse では、@message 内にある matchedData という配列の内容を、matched というフィールドとして表示します。 例えば、@message 内に
"matchedData":["message= ","AND","1","=","CONVERT"], というデータがあった場合、
"message= ","AND","1","=","CONVERT" を表示します。

また、pattern 構文を使用すると、matched フィールドに含まれるパターンの数や割合を分析することができます。

参考:CloudWatch Logs Insights クエリ構文 - Amazon CloudWatch Logs

余談

夜に丹沢山地を走ってきました。
丹沢ケルベロスというコースを夜中に走り切る、というマニアックな内容でした。
とても楽しかったです。

山本 哲也 (記事一覧)

カスタマーサクセス部のエンジニア。2024 Japan AWS Top Engineers に選んでもらいました。

今年の目標は Advanced Networking – Specialty と Machine Learning - Specialty を取得することです。

山を走るのが趣味です。今年の目標は 100 km と 100 mile を完走することです。 100 km は Gran Trail みなかみで完走しました。OSJ koumi 100 で 100 mile 砕け散りました。どこかで 100 mile やりたいです。

基本的にのんびりした性格です。座右の銘は「いつか着く」