5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

英語が苦手なエンジニアがセキュリティに挑んだ話〜SQLインジェクション編〜

Last updated at Posted at 2025-05-29

※前回の記事はこちらです。

ZooMにて

僕:その、今回からセキュリティに関わるっぽくて、その…。

先輩:はい、すでに聞いています。セキュリティについての事前知識はありますか?

僕:い、いやまったく…今回のSQLインジェクションっていうのも何か…SQLに悪さをされるイメージしかないです。

先輩:そうですか。じゃあまずSQLインジェクションについて軽く説明しますね。

僕:よ、よろしくお願いします…。

SQLインジェクションとは

先輩:まずSQLとはデータベース言語です。データベースに格納されているデータを抽出したり、追加・削除したりするために使います。

たとえば以下の命令文なら、「Usersテーブルからusernameが「Mimirin」である行のusernameカラムを抽出してください」という意味です。

SELECT username FROM Users WHERE username = 'Mimirin';

僕:はい。

先輩:これをもし赤の他人にされたらどうなると思いますか?

僕:システムに登録している人の情報が盗まれてしまいます。

先輩:そうです。しかもそれだけではなく内容を改竄されたり、すべてのデータを削除されることも起きますよね。

僕:そうですね…でも、usernameとpasswordを両方正しく入力しなければログインできませんし、赤の他人ができるはずは…。

先輩:それができるんですよ。それが 「SQLインジェクション」 という攻撃手法です。

僕:えっ、どうやって?

先輩:たとえばこんな命令文です。

SELECT * FROM users WHERE username = '' OR '1'='1';

僕:ん?「username が空文字、または'1'='1'のユーザーを取得してください」?

先輩:まず、*の部分でusers表にある全てのカラムを選択しています。そして、usernameが''、つまり、空欄のものを抽出させようとしていますね。

僕:はい、でもこれ何の意味があるんですか?

先輩:そもそもですが、usernameが空欄であることはまずありません。システム上で必ず設定しなければならない項目になっているからです。

僕:だからこれだと何も出ないですよね?

先輩:そう、これだけなら「WHERE username = ''」の部分は false になります。でも問題は次の「OR '1'='1'」なんです。

僕:「OR」ってことは、条件分岐?

先輩:そうです。条件分岐は、trueにならない限り処理を続行します。そして「'1'='1'」の部分はどうですか?

僕:「'1'='1'」っていうのはつまり、 true ってことですよね?だって1と1が一緒って当たり前ですもん。

先輩:そうです。つまりこの命令文はtrueとなり、usersテーブルすべてのカラムが取得できてしまう。 赤の他人に全ユーザーの情報を根こそぎ取得されてしまう んですよ。

僕:え、ちょっと待ってください、何でこんな命令文で取得できちゃうんですか?

先輩:そもそも、SQL文のWHEREってtrueかfalseかで判別しているんですよ。つまり、 「条件に当てはまるデータが存在するかしないか」 であって、trueのものを抽出し、falseのものを除外します。

今回、usernameが空欄の条件に当てはまるカラムを探してみましょう。usernameは必ず登録しないといけない項目なので、usersの表をくまなく探しても存在しない(false)から、この時点では何も返しません。

でも次の条件分岐にある「'1'='1'」はどうですか?そもそもこれ自体何があってもtrueですから、データベースの中身がわからなくても、データ全部(*の部分でusers表にある全てのカラムを選択しているので)が取得されます。

実質これと一緒ですね。

SELECT * FROM users;

僕:いや、じゃあ 「WHERE username = '' OR '1'='1'」自体いらないじゃないすか。

先輩:そう思うでしょ?でも違うんですよ。

僕:ええ…?(もうすでに混乱中)

WHERE username = '' OR '1'='1'がわざわざいる意味

先輩:そもそもSQLインジェクション攻撃を行う攻撃者の目的としては、主に以下が挙げられます。

  • 管理者権限を奪う
  • 個人情報を流出させる
  • データを改竄する
  • サービスを壊す

個人情報を奪うだけでなく、流出させたりデータを改竄したりすることもできます。

たとえば自分のアカウントへ勝手に管理者権限を与えたり、他のユーザーのクレジットカードの情報を盗んで、それを自分の支払先にすることもできてしまいます。

データベースのデータを全部DELETEすれば、サービスを破壊することもできますね。

僕:怖いっすね…。

先輩:今回脆弱性診断を行ったシステムにログインするには、usernameとpasswordを正しく入力する必要があります。SQL文に直すとこんな感じですよね。

SELECT * FROM users WHERE username = '入力値' AND password = '入力値';

攻撃者はこの仕組みを利用して、不正ログインを試みます。つまりこのSQL文に沿った形のSQL文を作成します。

SELECT * FROM users WHERE username = '' AND password = '' OR '1'='1'

でも、この場合 「false(usernameが空欄のカラムは無い)→false(passwordが空欄のカラムは無い)→true('1'='1'は宇宙の摂理)」 ですよね?falseが2つ出るってわかってるのに、必要なくないですか?

僕:ま、まあそうですね…。

先輩:だからこれでいいんですよ。

SELECT * FROM users WHERE username = '' OR '1'='1'

僕:でも意味的にはこれと一緒なんでしょう?

SELECT * FROM users;

先輩:意味的には、です。でも、SQLインジェクションはSQL文を「injection(注入)」するから、「SQLインジェクション」と呼ぶんです。

つまり、あくまで 「injection(注入)」できるSQL文でなければ意味がありません。

僕:はあ?

SQLインジェクションが有効になってしまうソースコード

先輩:たとえば、Go言語で書かれたシステム内にこんなソースコードがあるとします。

name := r.FormValue("username")
// HTTPリクエストのフォームデータから "username" という名前の値を取り出してnameという変数に代入
query := "SELECT * FROM users WHERE username = '" + name + "';"
// SQL文を直接記載して実行し、queryという変数に値を代入しようとしている

ソースコード上では、name = r.FormValue("username")、つまり username(カラム)=name(フォームから取ってきた値) であって欲しいですよね。

変数nameを使うために、あえて左右に+を入れ、文字列連結に書き換えるという抜け道を使っています。

だって以下みたいに書いてしまったら、usernameカラムとnameカラムを比較するという意味になっちゃいますからね。

query := "SELECT * FROM users WHERE username = name;"

僕:そうですねぇ…。

先輩:そこで、攻撃者はこのようなソースコードに着目し、不正ログインを試みます。

ログインフォームで、usernameのフォームに「' OR '1'='1」と入力するんです。

スクリーンショット 2025-05-29 午後2.21.52.png

すると、以下のSQL文が生成されます。
最初こそ「' OR '1'='1」という意味不明な記号ですが、SQL文生成時に''が加えられると…

SELECT * FROM users WHERE username = '' OR '1'='1';

有効なSQL文に化けましたね。

そして「'1'='1'」という何があってもtrueの条件がすべてのユーザーに当てはまるので、 usernameもpasswordも知らないのに、ログインできてしまうんです。

僕:えー!!

先輩:これが「SQLインジェクション」です。

SQLインジェクションを防ぐには?

僕:SQLインジェクションが恐ろしいのはよくわかりました。なら、これを防ぐにはどうすればいいんですか?

先輩:そうですね、主に以下の方法が有効です。

  • プリペアドステートメント(Prepared Statements)を使う
  • ORマッパー(ORM)を使う

僕:プリ…?

先輩:大丈夫です、ちゃんと説明しますから。

プリペアドステートメント(Prepared Statements)を使う

先輩:たとえばこんなふうに書くと良いです。

stmt, err := db.Prepare("SELECT * FROM users WHERE username = ?")
if err != nil {
    log.Fatal(err)
}
rows, err := stmt.Query(username)

Prepared(準備済みの)Statements(声明)、つまり 最初から変数を扱うSQL文を準備しておく方法です。

「?」 の部分が変数として安全に扱われるため、悪意ある文字列を入れてもSQL文に干渉できなくなります。

もしログイン画面で「' OR '1'='1」と入力しても、SQL文がこうなるので、

username = "『 OR 1=1 --』"

ただの文字列として判断されてSQL文として成立せず、攻撃できなくなるんです。

ちなみに、「?」や「:name」など、「ここにデータが入るよ」と示す記号のことを 「プレースホルダ(placeholder)」 と言います。

プリペアドステートメント(Prepared Statements)と「プレースホルダ(placeholder)は、一緒に使用されることが多いですね。

僕:へえ…。

ORマッパー(ORM)を使う

先輩:あとは直接SQL文を記載しない方法も有効ですね。
たとえばGORMのようなライブラリを使えば、以下のように書くことができます。

var user User
db.Where("username = ?", username).First(&user)

SQL文が無いのでSQLインジェクション攻撃自体がしにくくなります。

ただ、ORマッパー(ORM)を使ったとしても実際の処理時にSQL文を生成することに変わりないので、 書き方を間違えるとSQLインジェクションの餌食になる ことについては注意してください。

ZooMにて

僕:いや、初回から濃いですね…。

先輩:お疲れ様でした。あとは何に引っかかってますか?

僕:次は「クリックジャッキング攻撃」ですね。

先輩:わかりました、じゃあ次は「クリックジャッキング攻撃」について説明します。

僕:はい…。

(次回「クリックジャッキング攻撃編」へ続く)

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?