(Last Updated On: 2022年9月28日)

プログラムからCSSファイルにデータを書き込む場合、エスケープしないと不正なCSSになってしまう場合があります。現在のブラウザはCSSでプログラムを実行する仕様が削除されているのでコード実行脆弱性の問題になりませんが、不正なCSSはセキュリティ問題(クリックジャック等)になる場合もあります。

HTML内のCSSの場合、エスケープしないとJavaScriptの実行を許してしまいます。HTMLエスケープでJavaScript実行は防げますが、不正なCSSのインジェクションを防ぐにはCSSエスケープが必要です。適切に処理しないとCSSキーロガーを仕込まれて情報を盗まれたりします。

CSSのエスケープ仕様

CSSのエスケープ仕様の概要はW3Cの

https://www.w3.org/International/questions/qa-escapes

で解説されています。要点のみ紹介します。

HTMLコンテクストに書き込む場合はHTMLのエンティティ化をエスケープに利用できます。ユーロマーク(€)の場合は以下のようにエンティティ化(エスケープ)できます。

FormatName
€hexadecimal numeric character reference
€decimal numeric character reference
€named character reference

CSSパーサーに渡される前にHTMLパーサーがデコードしたデータを渡すのでHTMLエンティティ化でも正しく処理されます。この動作はHTMLエンティティ化はユーロマークなど表示しずらい文字の表示には利用できますが、セキュリティ対策用としては利用できないことを意味します。

※ HTMLコンテクストのCSSとはHTML中に記述されているCSS定義を指します。CSSコンテクストとはCSS”ファイル”の中のCSS定義です。

CSSコンテクストに書き込む場合はCSS用のエスケープを利用できます。HTML内のCSSコンテクストに書き込む場合も、CSS用のエスケープを利用する方が安全です。ユーロマーク(€)の場合は以下のようにエスケープできます。

FormatNotes
\20ACmust be followed by a space if the next character is one of a-f, A-F, 0-9
\0020ACmust be 6 digits long, no space needed (but can be included)

\ + 文字コード の形式でエスケープできます。ASCIIの127までは \XX の形式でもエスケープできます。\00XX, \0000XXとも書けます。続く文字でパーサーに誤解されないようにするには\XXXXXXの形式(6桁の文字コード)でエスケープするのと安全です。

\XX, \XXXXの形式で誤解を防ぐには後ろにスペースを付けると防げますが、通常は6桁のHEXを使う方が良いでしょう。

W3CのFAQ文書にも記載されていますが、基本的には後者(CSSエスケープ)を利用すべきです。これは、URIコンテクストはURIエスケープ(エンコード)を使用する、JSONコンテクストではJavaScript文字列エスケープを利用する場合と同じです。

※ HTMLコンテクストに対しても、ターゲット(CSS/URI/JavaScript文字列など)のコンテクストに対しても、両方に対して安全なエスケープ(エンコード)方法を利用するのがベストプラクティスです。

CSSの特殊文字の前に\を付けてエスケープ可能(例:\: )ですが、文字コードを利用したエスケープ(例: \00003A)をお勧めします。冗長ですがセキュアコーディングでは可能な限り安全性の高い方法でエスケープ(エンコード)が推奨されています。HEX6桁の場合、動作不良を起こしやすい全ての特殊文字をエスケープ(エンコード)できる上、続く文字列による曖昧さが残りません。

CSSコンテクストで特殊な意味を持つ文字

CSSコンテクストで意味を持つ文字=エスケープすべき文字は以下の文字です。

!"#$%&'()*+,-./:;<=>?@[\]^, _,`{|}~

空白文字(\r\n\v\f\t )も意味を持ちます。

攻撃用文字列などで不正に意味を変えられないようにする為にはエスケープが欠かせません。HTMLで意味を持つ<, >, ‘, “, &, /も含まれているのでCSSエスケープを行なえば、HTMLインジェクションも防止できます。

ヌル文字はバリデーションで排除されているべきですが、万が一含まれている場合はJavaScriptのCSS.escape()の様に代替文字に変換するのもやむを得ないかも知れません。

バリデーションが甘いアプリケーションの場合、作成したエスケープ関数でバリデーションしエラーにする事も可能です。しかし、遅すぎるバリデーションはアプリケーション状態の不整合を容易に起こすので可能な限り早い段階でのバリデーションが必要です。

JavaScriptでのCSSエスケープ

JavaScriptのCSSエスケープは簡単です。CSS.escape()を使うだけです。

https://developer.mozilla.org/en-US/docs/Web/API/CSS/escape

実行例

CSS.escape(".foo#bar")        // "\.foo\#bar"
CSS.escape("()[]{}")          // "\(\)\[\]\{\}"
CSS.escape('--a')             // "--a"
CSS.escape(0)                 // "\30 ", the Unicode code point of '0' is 30
CSS.escape('\0')              // "\ufffd", the Unicode REPLACEMENT CHARACTER

お勧めする6桁文字コードを利用する方法でエスケープしていませんがこれもでも凡そ大丈夫です。

他の言語でのCSSエスケープ

APIが在ればそれを利用すれば良いですが、Web用のフレームワークでもCSS用のエスケープAPIを実装していないフレームワークが少なくないです。実装されていても不完全な場合もあるので上記のエスケープされるべき文字が全てエスケープされているか確認すると良いです。

お勧めのエスケープ方法は英数字以外の文字を全てエスケープ(エンコード)してしまう方法です。単純に英数字以外を全てCSSエスケープ(エンコード)する場合はpreg_replace_callback()で簡単に置き換えれます。

<?php

function escape_css($str){
	$f = function ($m) {
		if ($m[0] === "\0") {
			return '\\00FFFD';
		}
		return sprintf (
			'\\%06X',
			unpack("Nc", mb_convert_encoding($m[0], "UTF-32BE"))["c"]
		);
	};

	if (is_numeric($str)) {
		return $str;
	}
	return preg_replace_callback('/[^0-9a-z]/iSu', $f, $str);
}

echo escape_css(".foo#bar").PHP_EOL;
echo escape_css("()[]{}").PHP_EOL;
echo escape_css("--a").PHP_EOL;
echo escape_css(0).PHP_EOL;
echo escape_css("\0").PHP_EOL;

出力

\00002Efoo\000023bar
\000028\000029\00005B\00005D\00007B\00007D
\00002D\00002Da
0
\00FFFD

CSS.escape()のように特殊文字だけをエスケープ(エンコード)する場合も文字列変換関数などを用いればAPIが無くても容易にエスケープ 関数を作成できます。

<?php

function escape_css($str) {
	$map = [
		"\t" => '\\000009',
		"\n" => '\\00000a',
		"\v" => '\\00000b',
		"\f" => '\\00000c',
		"\r" => '\\00000d',
		" " => '\\000020',
		'!' => '\\000021',
		'"' => '\\000022',
		'#' => '\\000023',
		'$' => '\\000024',
		'%' => '\\000025',
		'&' => '\\000026',
		'\'' => '\\000027',
		'(' => '\\000028',
		')' => '\\000029',
		'*' => '\\00002a',
		'+' => '\\00002b',
		',' => '\\00002c',
		'-' => '\\00002d',
		'.' => '\\00002e',
		'/' => '\\00002f',
		':' => '\\00003a',
		';' => '\\00003b',
		'<' => '\\00003c',
		'=' => '\\00003d',
		'>' => '\\00003e',
		'?' => '\\00003f',
		'@' => '\\000040',
		'[' => '\\00005b',
		'\\' => '\\00005c',
		']' => '\\00005d',
		'^' => '\\00005e',
		'_' => '\\00005f',
		'`' => '\\000060',
		'{' => '\\00007b',
		'|' => '\\00007c',
		'}' => '\\00007d',
		'~' => '\\00007e',
		"\0" => '\\00fffd',
	];
	
	if (is_numeric($str)) {
		return $str;
	}
	return strtr($str, $map);
}


echo escape_css(".foo#bar").PHP_EOL;
echo escape_css("()[]{}").PHP_EOL;
echo escape_css("--a").PHP_EOL;
echo escape_css(0).PHP_EOL;
echo escape_css("\0").PHP_EOL;

出力

\00002efoo\000023bar
\000028\000029\00005b\00005d\00007b\00007d
\00002d\00002da
0
\00fffd

投稿者: yohgaki