検索フォームに名前を入れると、その人の情報が出てくる。よくある機能です。ここに名前ではなく ' OR '1'='1 という文字列を入れると、登録されている全員の情報が返ってくることがあります。
今回は実際に手を動かしました。脆弱なコードを書いて動かし、何件返ってくるかを数えています。結果を先に言うと、3人分のデータが返り、その中には管理者アカウントも含まれていました。そこから、修正が1箇所で済むことまでを見ていきます。
起きているのは、入力した文字列がデータではなく命令として読まれることです。名前を入れる欄が、そのまま検索条件を書き換える欄になってしまいます。
SQLインジェクションは、アプリがデータベースへ送る命令文(SQL)に、利用者の入力がそのまま混ざってしまう問題です。名前で検索する機能を例にすると、次のような命令文が作られます。
SELECT id, name, email FROM users WHERE name = 'Alice'
問題は、この Alice の部分を入力からそのまま埋めている場合です。入力に引用符が含まれていると、条件式そのものを書き換えられます。
検索欄に Alice と入れたときは、名前が Alice の行だけが条件に当てはまります。ここまでは意図したとおりの動きです。
たとえば ' OR '1'='1 と入力すると、命令文は「名前が空、または 1と1が等しい」という形になります。1と1は常に等しいので、条件は全ての行に当てはまります。
同じ入力欄でも、値として扱えばデータのまま。連結すると命令の一部になる。
OWASPは、入力値の検証を厳しくする方法と比べて、パラメータ化の方が保守の手間が少なく、安全性の保証も大きいとしています。詳しくは次の節で実際に動かして確かめます。
この記事で使う入力は ' OR '1'='1 という文字列だけです。専用のツールも、通信を書き換える技術も要りません。フォームに文字を打つだけで試せてしまうところが、この脆弱性の厄介な点だと編集部は考えています。
parameterized SQL statements require less maintenance and can offer more guarantees with respect to security原文OWASP Community「SQL Injection」 この内容の有効期限2027-02-17
実際に脆弱なコードを書いて動かしました。攻撃の入力を1つ入れただけで、返る件数が1件から3件に増え、その中に管理者が含まれていました。
SQLインジェクションは文章で説明するより動かした方が早いので、手元で試しました。会員が3人(Alice・Bob・Admin)だけの小さなデータベースを作って、名前で検索する関数を2つ用意します。
下のコードは読み飛ばして大丈夫です。違いは1点だけで、片方は入力をSQLに直接つないでいて、もう片方は値として渡していることです。
// 危険: 入力を文字列連結でSQLに埋める
function findUnsafe(name) {
const sql = `SELECT id,name,email,is_admin FROM users WHERE name = '${name}'`;
return db.prepare(sql).all();
}
// 安全: プレースホルダに値を渡す
function findSafe(name) {
return db.prepare('SELECT id,name,email,is_admin FROM users WHERE name = ?').all(name);
}
--- 普通の検索(名前に Alice を入力)--- 組み立てたSQL: SELECT id,name,email,is_admin FROM users WHERE name = 'Alice' 結果: 1件 Alice --- 攻撃の入力(名前に ' OR '1'='1 を入力)--- 組み立てたSQL: SELECT id,name,email,is_admin FROM users WHERE name = '' OR '1'='1' 結果: 3件 Alice, Bob, Admin --- 同じ入力をパラメータ化した関数へ --- 結果: 0件 (なし)
返ってきました。3件です。しかも Admin が含まれています。名前を1つ検索したつもりが、会員全員の一覧になりました。
組み立てられたSQLを見ると理由が分かります。name = '' OR '1'='1' となっていて、後半が常に成り立つので、全ての行が条件に当てはまります。
一方、同じ入力をパラメータ化した関数に渡すと0件でした。こちらでは ' OR '1'='1 という文字列が、そのまま「名前」として扱われます。そんな名前の人はいないので、何も返りません。
OWASPは、パラメータ化を使うと開発者はSQLを先に全部書き、値は後から渡す形になると説明しています。命令文の形が先に確定するので、後から渡す値がその形を変えることはできません。
+ でSQLを作っている処理が対象parameterized queries force the developer to define all SQL code first and pass in each parameter to the query later原文OWASP Cheat Sheet Series「SQL Injection Prevention」 この内容の有効期限2027-02-17
確かめ方は単純です。検索欄に攻撃の文字列を入れて、返る件数が増えないかを見るだけ。増えたら、そこに穴があります。
SQLインジェクションの確認で見るべき点は1つだけです。OWASPは、パラメータ化しておけば攻撃者が命令の意図を変えることはできない、としています。つまり「意図が変わらないか」を確かめればよいことになります。
' OR '1'='1 を入れて送る件数が増えず、エラーも出ないなら、その入り口については問題ありません。ただし入力を受け取る場所は1つではないので、検索欄・絞り込み・並び替え・URLの数値と、順に確かめていくことになります。
パラメータ化が済んだと言えるのは3条件すべてを満たしたとき。生SQLの残りが最も多い。
認可の考え方はIDORの記事でも扱っています。SQLインジェクションが「命令を書き換えられる」問題なのに対し、あちらは「命令は正しいが、誰のデータかを見ていない」問題です。
この確認が効くのは、実施した時点までです。機能を足すたびに入力を受け取る場所は増え、そのたびに同じ穴が開きます。手作業の確認は最初の棚卸しに使い、以降はテストコードに任せるのが現実的です。攻撃の文字列を入れて件数を数えるだけなので、自動テストにも落としやすい部類です。
prepared statements ensure that an attacker cannot change the intent of a query, even if SQL commands are inserted by an attacker原文OWASP Cheat Sheet Series「SQL Injection Prevention」 この内容の有効期限2027-02-17
' OR '1'='1 の1入力で全3件(管理者含む)が返った' OR '1'='1 を入れて、件数が増えないことを確かめる同じ課題を持つ会社にとって、動いている設定は「作る時間」を買えるということです。ServiceDockは自作のワークフローやテンプレートを出品できるマーケットプレイスです。手数料や出品の流れは出品者向けページにまとまっています。
出品の仕組みを見る