1.表題
JavaScriptでhrefへURLを設定時に不正なURLが設定され、クリック時に不正な実行されないようにする際の処理について記載します。
いわば、XSS攻撃の防御になります。
以下で紹介されているものに近い内容です
https://www.ipa.go.jp/security/vuln/websecurity/cross-site-scripting.html
2.背景
HTMLで出力される以下のようなhref文において
<a href="https://example.com/">ここをクリックする</a>
上記のような固定長のものであれば通常のページ遷移がされ、問題ないですが、
URLが以下のように動的で設定される変数の場合に問題が起きることがあります。
<a href="<%= url %>">ここをクリックする</a>
もしurlの取得元がブラウザのRefererやクエリパラメータで渡されるURLの変数の場合
【クエリパラメータで渡されるURLの場合】
<%
String url = request.getParameter("url");
%>
or
【Refererで渡されるURLの場合】
<%
String url = (String)session.getAttribute("Referer");
%>
urlやRefererに
javascript:alert('アラートが表示されました');
のようなものが設定されていた場合には、
HTMLに出力時に以下のように出力されます。
<a href="javascript:alert('アラートが表示されました');">ここをクリックする</a>
上記で出力されたHTMLは以下のようになります。
意図せぬスクリプト文が設定され、クリック時には、ブラウザ上でスクリプトが実行されてしまいます。
そのため、hrefへ設定時に、
(不正な文字=javascript:)が先頭に設定されている場合は、不正な文字を取り除き設定させる必要があります。
以下のように修正が検討されます。
3.HTML側(JSPのHTML出力部)
Javaで行う関数(CheckModule内のsafeUrlNotScript)を呼び出した形にする。
<a href="<%= CheckModule.safeUrlNotScript(url) %>">ここをクリックする</a>
4.Java側
以下のモジュールを使用することにより、安全なurlへ変換します。
空白で返却ではなくて、#で返却することにより画面を同一画面を静止する目的があります。
また、windows.open()にも対応できます。
※クラス名の宣言などは割愛
import java.util.regex.Pattern;
import java.util.regex.Matcher;
// 正規表現で、制御文字はじまりで「javascript:」の場合(小文字大文字問わない)を示す。
// また、「javascript:」の各文字の中で改行やタブが行われても検知するようにします。
private static final Pattern JAVA_SCRIPT_PROTOCOL_REGEX_PATTERN = Pattern.compile(
"^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:",
Pattern.CASE_INSENSITIVE
);
/**
* JavaScriptプロトコルを含まないURLを返します。
* <p>
* 値が {@code "javascript:"} で始まる場合 {@code "#"} を返します。
* そうではない場合、受け取った値をそのまま返します。
* <p>
* 出力結果例:
*
* <pre>
* safeUrlNotScript(null) = null
* safeUrlNotScript("") = ""
* safeUrlNotScript("index.html") = "index.html"
* safeUrlNotScript("https://example.com/") = "https://example.com/"
* safeUrlNotScript("javascript:") = "#"
* safeUrlNotScript("JAVASCRIPT:") = "#"
* safeUrlNotScript("javascript:alert(1);") = "#"
* safeUrlNotScript(" javascript:alert(1);") = "#"
* safeUrlNotScript("\njavascript:alert(1);") = "#"
* safeUrlNotScript("\u0000javascript:alert(1);") = "#"
* safeUrlNotScript("javascript\n:alert(1);") = "#"
* safeUrlNotScript("xjavascript:alert(1);") = "xjavascript:alert(1);"
* safeUrlNotScript("https://example.com/?next=javascript%3Aalert%281%29%3B") = "https://example.com/?next=javascript%3Aalert%281%29%3B"
* safeUrlNotScript("#\" onclick=\"location.href='javascript:alert(1);'") = "#\" onclick=\"location.href='javascript:alert(1);'"
* </pre>
*
*
* @param url URL
* @return URL、もしくは {@code "#"}
*/
static public String safeUrlNotScript(String url)
{
if (url == null)
return null;
// 「javascript:」(大文字・小文字関係ない)と合致する場合はURL自体を無効としたいため、「#」で返します。
Matcher m = JAVA_SCRIPT_PROTOCOL_REGEX_PATTERN.matcher(url);
if (m.find())
return "#";
else
return url;
}
5.Java側(Junitファイル)
上記の関数をテストする際にJunit4でのテストも以下のように行いました。
パターンはJSプロトコル有りとJSプロトコル無しとNULL時の挙動確認となります。
※クラス名の宣言などは割愛
/**
* JSプロトコルが含まれると判定されることの確認
*/
@Test
public final void test_JSプロトコルが含まれると判定されることの確認()
{
List<String> inputs = new ArrayList<String>()
{
{
// 【小文字】
// 一般的な「javascript:」の場合。([\u0000-\u001F ]の文字が設定なし)※完全一致
add("javascript:");
// 一般的な「javascript:alert(1);」の場合。([\u0000-\u001F ]の文字が設定なし)※部分一致
add("javascript:alert(1);");
// 先頭にスペースを含む場合。([\u0000-\u001F ]の文字が設定あり)
add(" javascript:alert(1);");
// 「javascript:」の「j」と「:」以外の各文字の前に「\r」が設定された場合。
add("j\ra\rv\ra\rs\rc\rr\ri\rp\rt\r:alert(1);");
// 「javascript:」の「j」と「:」以外の各文字の前に「\n」が設定された場合。
add("j\na\nv\na\ns\nc\nr\ni\np\nt\n:alert(1);");
// 「javascript:」の「j」と「:」以外の各文字の前に「\t」が設定された場合。
add("j\ta\tv\ta\ts\tc\tr\ti\tp\tt\t:alert(1);");
// 「javascript:」の「j」と「:」以外の各文字の前に「\r」「\n」「\t」が設定された場合。
add("j\r\n\ta\r\n\tv\r\n\ta\r\n\ts\r\n\tc\r\n\tr\r\n\ti\r\n\tp\r\n\tt\r\n\t:alert(1);");
// [\u0000-\u001F ]の確認
add("\u0000javascript:alert(1);");
add("\u001Fjavascript:alert(1);");
add("\u0000\u001Fjavascript:alert(1);");
add("\u0000\u001Fjavascript:alert(1);");
add("\u0000\u0010\u001F javascript:alert(1);");
// 【大文字】
// 一般的な「JAVASCRIPT:」の場合。([\u0000-\u001F ]の文字が設定なし)※完全一致
add("JAVASCRIPT:");
// 一般的な「JAVASCRIPT:ALERT(1);」の場合。([\U0000-\U001F ]の文字が設定なし)※部分一致
add("JAVASCRIPT:ALERT(1);");
// 先頭にスペースを含む場合。([\U0000-\U001F ]の文字が設定あり)
add(" JAVASCRIPT:ALERT(1);");
// 「JAVASCRIPT:」の「J」と「:」以外の各文字の前に「\r」が設定された場合。
add("J\rA\rV\rA\rS\rC\rR\rI\rP\rT\r:ALERT(1);");
// 「JAVASCRIPT:」の「J」と「:」以外の各文字の前に「\n」が設定された場合。
add("J\nA\nV\nA\nS\nC\nR\nI\nP\nT\n:ALERT(1);");
// 「JAVASCRIPT:」の「J」と「:」以外の各文字の前に「\t」が設定された場合。
add("J\tA\tV\tA\tS\tC\tR\tI\tP\tT\t:ALERT(1);");
// 「JAVASCRIPT:」の「J」と「:」以外の各文字の前に「\r」「\n」「\t」が設定された場合。
add("J\r\n\tA\r\n\tV\r\n\tA\r\n\tS\r\n\tC\r\n\tR\r\n\tI\r\n\tP\r\n\tT\r\n\t:ALERT(1);");
// [\U0000-\U001F ]の確認
add("\u0000JAVASCRIPT:ALERT(1);");
add("\u001FJAVASCRIPT:ALERT(1);");
add("\u0000\u001FJAVASCRIPT:ALERT(1);");
add("\u0000\u001FJAVASCRIPT:ALERT(1);");
add("\u0000\u0010\u001F JAVASCRIPT:ALERT(1);");
// 【小文字】+ 【大文字】
// 「JaVaScRiPt:」のいずれかの文字の前にだけ「\t」が設定あり
add("JaVaScRiP\tt:alert(1);");
// 「JaVaScRiPt:alert(1);」の場合。
add("JaVaScRiPt:alert(1);");
}
};
for (String input : inputs)
{
String url = CheckModule.safeUrlNotScript(input);
assertEquals("#", url);
}
}
/**
* test_JSプロトコルが含まれないと判定されることの確認
*/
@Test
public final void test_JSプロトコルが含まれないと判定されることの確認()
{
final String JS_SMALL = "javascript:alert(1);"; // JavaScriptのBASE
final String JS_BIG = "JAVASCRIPT:ALERT(1);"; // JavaScriptのBASE
final String BASE_URL = "https://example.com/"; // 一般的なURL
final String FILE_NAME = "index.html"; // ファイル名
List<String> inputs = new ArrayList<String>()
{
{
// 空文字
add("");
// 【小文字】
// 一般的なURLの場合。
add(BASE_URL);
// 一般的なファイル名の場合。
add(FILE_NAME);
// 任意の文字 +「javascript:alert(1);」+ 一般的なURL + ファイル名の場合。
add("x" + JS_SMALL + BASE_URL + FILE_NAME);
// 任意の文字 +「 javascript:alert(1);」+ 一般的なURL + ファイル名の場合。
add("x" + " " + JS_SMALL + BASE_URL + FILE_NAME);
// 【大文字】
// 一般的なURLの場合。
add(BASE_URL);
// 任意の文字 +「JAVASCRIPT:ALERT(1);」+ 一般的なURL + ファイル名の場合。
add("X" + JS_BIG + BASE_URL + FILE_NAME);
// 任意の文字 +「 JAVASCRIPT:ALERT(1);」+ 一般的なURL + ファイル名の場合。
add("X" + " " + JS_BIG + BASE_URL + FILE_NAME);
}
};
for (String input : inputs)
{
String url = CheckModule.safeUrlNotScript(input);
assertEquals(input, url);
}
}
/**
* test_nullが入力された際の挙動の確認
*/
@Test
public final void test_nullが入力された際の挙動の確認()
{
List<String> inputs = new ArrayList<String>()
{
{
add(null);
}
};
for (String input : inputs)
{
String url = CheckModule.safeUrlNotScript(input);
assertEquals(null, url);
}
}
6.終わりに
今回はドメインなしのファイル名での移動もあるため、「http://」や 「https://」でチェックせずに、「javascipt:」を検知したら、#へ置き換えとしましたが、
IPAの公式で紹介されている「URLを出力するときは、「http://」や 「https://」で始まるURLのみを許可する。」という観点で作っても良いかもしれないです。
XSSは基本的なことで有りますが忘れがちかつ忘れるととんでもない攻撃をされることにもなるため、意識した作りにしたいものです。