本記事は、Javaのセキュアコーディングについてのメモとして書かれています。
主にアスキー・メディアワークスから出版されている「Javaセキュアコーディングスタンダード CERT/ Oracle版」を中心に学習をしていきます。
今回は、SQLインジェクションについて学んでいきます。
SQLインジェクションとは
SQLインジェクション(SQL Injection)とは、WEBアプリケーションなどにおいて、連携しているデータベースへ問い合わせ(クエリ)を行う際に、悪意のあるSQL文を送信し、データベースを不正利用する攻撃を言います。
インジェクションとは「注入」という意味で、SQLインジェクションの他にもOSコマンドインジェクション、HTTPヘッダインジェクション、メールヘッダインジェクションなど様々なインジェクション攻撃があります。
ちなみに、OWASP(Open Web Application Security Project)が発表している「最も重大なウェブアプリケーションリスクトップ10」では、2013年と2017年の調査で何れもインジェクション攻撃が1位となっています。
データベースへの問い合わせ方法
WEBアプリケーションからデータベースへ問い合わせをする際には、以下の何れかの方法でSQL文を組み立てる必要があります。
- 文字列連結を利用した組み立て
- プレースホルダを利用した組み立て
上記の中で「1.文字列連結を利用した組み立て」の方法を利用した際にSQLインジェクションによる攻撃を受けやすくなります。
まず、文字列連結を利用した方法について確認してみます。
文字列連結を利用したSQL文の組み立て
まずログイン認証画面でユーザ名とパスワードを入力し、その情報をデータベースに問い合わせるプログラムを想定してみます。
以下はSQL文を組み立てて、データベースに問い合わせをし、その結果を真偽値で返すメソッドになります。
public boolean login(User user) { try(Connection conn = DriverManager.getConnection( JDBC_URL, DB_USER, DB_PASS)){ String sql = "select user_id, pass from Account where user_id='" + user.getUser() + "' and pass='" + user.getPass() + "';"; Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql); if (rs.next()) { return true; } else { return false; } } catch(SQLException e) { e.printStackTrace(); return false; } }
上記の4、5行目がSQL文の組み立てを行っている箇所になります。
Userクラスのフィールドにセットされているユーザ名及びパスワードをWHERE句に指定し、Accountテーブル(アカウント情報を登録しているテーブル)からSELECT文を実行します。
SELECT文の結果があればtrue
を、なければfalse
を返します。
この時に、ユーザ名とパスワードを以下のように指定したらどうなるでしょうか。
ユーザ名:user' OR '1'='1 パスワード:pass' OR '1'='1
この際に組み立てられるSQL文は以下になります。
select user_id, pass from Account where user_id='user' OR '1'='1' and pass='pass' OR '1'='1';
まず、WHERE句ではuser_idとpassをANDで評価しているため、それぞれの結果が一致しなければtrue
とはなりません。
そこで、user_idとpassのそれぞれにおいて、ORを追加し「1=1」を加えることで、true
になるように調整をしています。
結果としては「true
AND true
= true
」なので、ログイン認証が成功してしまいます。
これらの対策として、後述するプレースホルダを利用した組み立てを行います。
プレースホルダを利用したSQL文の組み立て
プレースホルダ(place holder)では、 SQL文の中で値を入れたい個所に記号を置き、後にその記号に実際の値を割り当てる処理を行います。
プレースホルダには以下の2通りの方法があります。
動的プレースホルダは、アプリケーション側でパラメータのバインド処理を行います。
これに対して静的プレースホルダは、データベースエンジン側でバインド処理を行います。
なお、SQLのISO/JIS規格では静的プレースホルダは準備された文(Prepared Statement:プリペアドステイトメント)と呼びます。
これは、予めSQL文の構造や条件などの処理しておくことで、あとから条件の追加や構造の変更をできなくしてしまうことです。
(本来は、時間を要するSQL文の構造や条件の解釈を先に処理し、プレースホルダ部分を後で差し替えることで、処理性能自体を向上させるために使われていた技術になります)
一般的には、動的プレースホルダはアプリケーションのライブラリ側でのバインド処理に起因した脆弱性が完全には取り除けないとの観点から、静的プレースホルダを利用したほうが安全と言われています。
それでは、上記のlogin
メソッドを静的プレースホルダを利用したものに置き換えてみます。
public boolean login(User user) { try(Connection conn = DriverManager.getConnection( JDBC_URL, DB_USER, DB_PASS)){ String sql = "select user_id, pass from Account where user_id=? and pass=?"; PreparedStatement pStmt = conn.prepareStatement(sql); pStmt.setString(1, user.getUser()); pStmt.setString(2, user.getPass()); ResultSet rs = pStmt.executeQuery(); if (rs.next()) { return true; } else { return false; } } catch(SQLException e) { e.printStackTrace(); return false; } }
上記の4~7行目が静的プレースホルダを利用したSQL文の組み立てとなり、?
部分をuser_id及びpassに置き換えます。
このように処理することで、たとえ上記のようなユーザ名とパスワードを入力したとしても、意図したようなクエリは実行できなくなります。
また、個人的にはプレースホルダを利用したほうがコード自体もすっきりとして読みやすくなった印象があります。
最後に
今回はインジェクション攻撃でも最もメジャーなSQLインジェクションの方法と対策について学びました。
今回はログイン処理を例にしましたが、データの挿入や更新など、クエリが発生する様々な場所でも静的プレースホルダを適用しなければなりません。
参考書籍
Javaセキュアコーディングスタンダード CERT/ Oracle版