RAKUS Developers Blog

株式会社ラクスのエンジニアブログ

要注意!新人エンジニアが発生させた2大脆弱性

はじめに

記事をご覧のみなさん、はじめまして。新卒1年目エンジニアのkasuke18と申します。ブラウザは圧倒的にChrome派です。

今回の記事では私が作ってしまった2大脆弱性XSSSQLインジェクションについて、ソース例を踏まえて原因を追及します。なお、ソースの言語についてはPHPを利用しています。

この記事が初めてのブログ投稿ですので、「この書き方はヘンだな」と感じるときがあると思いますが、そこは温かい目で見守ってくださると幸いです。。

もくじ

この記事の想定する読者

新人エンジニアの方々

「2大脆弱性は知ってるけど、具体的にダメな実装例が実感できない」という方が読まれた際に、理解の手助けとなれば幸いです。

つきましては「XSSとは」「SQLインジェクションとは」ということは今回はさらっと流します。それらをお探しの方は他のサイトをご覧ください。IPAのサイトは国の機関なので信頼できると思われます。

「こんなパターンで脆弱性が発生するのか」や「ここの実装するときは気を付けないと!」などと感じていただくのが目的です。とはいっても実際のソースは大人の事情で載せられないので、実際のソースとは少しズレてしまいますが、そこはご容赦ください。。

XSS

XSSとは

いきなりですが、新人エンジニアの私から見てもわかりやすくまとまっていると感じた記事があるので引用します。「XSSとは何か」ということをお探しの方はこちらをご確認ください。

XSS(Cross Site Scripting)は、あるWebページにアクセスした標的のブラウザ上で、攻撃者が任意のコードを実行し得るバグ、あるいはそれを用いた攻撃手法のこと

XSSが発生したソース例

XSSが発生した原因などは次で説明するとして、まずはXSSが発生してしまったソース例を示します。

<!DOCTYPE>
<html>
  <head>
  </head>
  <body>
    <form action="" method="post">
      <!-- ↓ここでXSSが発生 -->
      <input name="hoge" type="hidden" value="<?php echo $_POST['hoge']; ?>">
    </form>
  </body>
</html>

この時実現したかったことは、フォームから入力値を受け取り、その入力値をパラメータにセットすることです。

なぜXSSが発生したのか

ソース例を見ていただければわかるかと思いますが、直接の原因は単純にエスケープが漏れていたことです。言い方は悪いですが半ば思考停止気味に「とりあえずエスケープしておこう」で防ぐことはできたミスです。

ではなぜエスケープが漏れてしまったのかと言い訳をしますと、単純に注意不足でエスケープをしなかったことに加えて、「入力値検証をしっかりと行っているから大丈夫」という無意識のうちに安心感を持っていたことです。

むしろ今回の件では見事にその意識の甘さが脆弱性を生み出しました。どういうことかと申しますと、具体的には以下の流れで発生していました。

  1. Chromeのdeveloperツールでパラメータを変更しスクリプトを仕込み、確定する
  2. サーバは入力値検証を行い、エラーと入力値を返す
  3. 戻った画面では受け取った入力値をセットする
  4. XSSが発生!!

今回XSSを発生させてしまったのは<input>type="hidden"パラメータでした。これは通常の画面操作からは直接変更できませんが、developerツールではできてしまいます。

それでは好き放題に変更された入力値を受け入れてしまうので、普通は入力値検証を行い、決められた値以外は許可しないようにします。

また、ユーザの使いやすさを考えると、エラー時に戻った画面ですでに入力されていた内容をセットしておくことも必要です。(入力内容を1か所間違えただけで全部やり直しというのはあまりに不親切ですので。)

今回の件でもそれに乗っ取り入力値検証を行い、数値以外は許可しないようにしていました。また、エラー時に入力値をセットするということもしていました。XSSを発生させる原因となった処理はこの「エラー時に入力値をセットする」という処理で、この入力値を出力する際にエスケープしなかったためにXSSが発生しました。

実施した対処

今回の件ではエスケープが漏れていたことが原因なので、エスケープを実施して対処しました。PHPではhtmlspecialchars()という関数を利用することでエスケープできます。

以下が修正後のソース例となります。

<!DOCTYPE>
<html>
  <head>
  </head>
  <body>
    <form action="" method="post">
      <!-- ↓ htmlspecialchars() を使用 -->
      <input name="hoge" type="hidden" value="<?php echo htmlspecialchars($_POST['hoge']); ?>">
    </form>
  </body>
</html>

この件を通して感じたこと

このXSS脆弱性は被害という意味ではほぼ問題ないと考えています。その理由は、

  1. 入力値検証ではじかれるため、サーバがスクリプトを許容していない

  2. 1を考慮すると、スクリプトが実行されるのはスクリプトを埋め込もうとした攻撃者のみである

以上の2点から被害はほぼないと考えました。

しかし、エスケープが漏れてしまっていたことは問題なので、今後は忘れないように十分に注意します。

SQLインジェクション

SQLインジェクションとは

XSS同様にSQLインジェクションも詳しい説明は省略させていただきます。詳しくはIPAの資料をご覧ください。

SQLインジェクションが発生したソース例

SQLインジェクションが発生した原因などは次で説明するとして、まずはSQLインジェクションが発生してしまったソース例を示します。

<?php
$id = $_POST['id'];
$status = $_POST['status'];
$conditions = '';
$isFirst = true;
if($id !== null){
  if($isFirst){ 
    $conditions .= ' WHERE ';
    $isFirst = false;
  } else { 
    $conditions .= ' AND ';
  }
  $conditions .= 'id = :id';
}
if($status !== null){
  if ($isFirst) { 
    $conditions .= ' WHERE ';
    $isFirst = false;
  } else { 
    $conditions .= ' AND ';
  }
  // ここでSQLインジェクションが発生
  $conditions .= 'status = ' . $status;
} 
$sql = 'SELECT is, name, status FROM table'. $conditions;
$sth = $dbh->prepare($sql);
$sth->bindValue(':id', $id);
$sth->execute();
?>

この時実現したかったことは、フォームから入力値を受け取り、その入力値によりDB検索を行うことです。また、フォームで入力されるパラメータは毎回すべて入力されるわけではないので、SQLのWHERE句は動的に組み立てる必要があります。

なぜSQLインジェクションが発生したのか

上記のソース例を見ていただければ一目でわかりますが、パラメータidは正しくプリペアードステートメントが使用されていますが、パラメータstatusは単なる文字列結合となってしまっています。このことがSQLインジェクションを発生させていました。

また間接的な原因として、パラメータstatusについての入力値検証が全く行われていなかったことがあります。ただの言い訳でしかありませんが、入力値検証を行わなかった理由はパラメータstatusの入力方法がプルダウンメニューであり、「プルダウンならプログラムが用意した値以外は送られてこない」という誤った認識をもってしまっていたからです。すでに上記XSSの項でも述べていますが、Chromeなどのdeveloperツールを利用すれば簡単に値を変更できることを失念していました。

実施した対処

理想論でいえば、本来は保険的な対策でしかない入力値検証だけで対処するのではなく、適切にプリペアードステートメントを利用することが必要です。しかし今回の件では諸事情により根本的な原因であるSQLの組み立て部分を変更することはできませんでした。よって入力値検証で数値以外は許容しないようにして対処しました。

XSSと同様、修正後のソース例を書こうと考えましたが、大人の事情で書くことができません。 代わりに、理想的な対処法であるプリペアードステートメントを適切に利用して対処したソース例を以下に示しますので、ご容赦ください。

<?php
$id = $_POST['id'];
$status = $_POST['status'];
$conditions = '';
$isFirst = true;
if($id !== null){
  if($isFirst){ 
    $conditions .= ' WHERE ';
    $isFirst = false;
  } else { 
    $conditions .= ' AND ';
  }
  $conditions .= 'id = :id';
}
if($status !== null){
  if ($isFirst) { 
    $conditions .= ' WHERE ';
    $isFirst = false;
  } else { 
    $conditions .= ' AND ';
  }
  // プレースホルダを使用
  $conditions .= 'status = :status';
} 
$sql = 'SELECT is, name, status FROM table'. $conditions;
$sth = $dbh->prepare($sql);
$sth->bindValue(':id', $id);
// 値をパラメータにバインドする
$sth->bindValue(':status', $status);
$sth->execute();
?>

この件を通して感じたこと

今回の件では「プルダウンなら入力値検証を行わくてもよい」という完全に誤った認識が露呈しました。プルダウンだけではなく、XSSの項でも挙げている<input>type="hidden"パラメータも同様で、「通常の画面操作から直接変更できないパラメータこそ入力値検証が必要」と感じました。

おわりに

以上が、私が実装してしまった2大脆弱性です。この記事の目的の通り、「こんなパターンで脆弱性が発生するのか」と把握していただけたでしょうか。実際のソースが載せられないため、正直よくあるソース例になってしまった感が否めません。。

ちなみにと言っては何ですが、今回ご紹介した脆弱性はどちらも単体テストで発見・修正しているので、大事には至っておりません。

最後までお読みいただきありがとうございました。

参考文献

Copyright © RAKUS Co., Ltd. All rights reserved.