2016年12月22日
最も割高なアンチパターン : 構造化されたデータを文字列関数で操作する「printfアンチパターン」について
(2015-7-22)by Igor Null
本記事は、原著者の許諾のもとに翻訳・掲載しております。
本記事では、私の知る最も割高なアンチパターンとなるプログラミングについて述べます。
それは、 構造化されたデータフォーマットを文字列関数を使って操作すること です。
以後これを” printfアンチパターン “と称します。
コスト
私がこれを”最も割高な”アンチパターンと呼ぶのは、根拠のない主張ではありません。
cve.miter.org のデータを使って 脆弱性をタイプ別にカウントし 、下記のように、上位を占める脆弱性のタイプ別リストを作りました。
rexec: 19268
DoS: 14849
xss: 9236
memory: 8212
sqlinj: 6230
privilege: 3321
dirtraversal: 2762
arith: 1260
csrf: 1117
私の方法に対する批判や良いご提案があれば遠慮なくどうぞ。
上位を見ると、XSSとSQLインジェクションの数が大きく目立っています。
私が言いたいのは、「ほとんどのXSSとSQLインジェクションはprintfアンチパターンが引き起こしている」ということです。
HTMLへ手当たりしだいに文字列を入れ込もうというのは、ひどい発想です。SQLについても同様です。
実例
printfアンチパターンの定義が分かると、それが 偏在していること であると気付くでしょう。
このアンチパターンはHTMLとSQLにおいて極めてありふれており、これがSQLインジェクションやXSS脆弱性が非常に多い理由です。
以下にいくつか例を挙げます。
- 現存するほとんどのPHP Webサイト
<div class="greeting">Hello, <?php echo $username; ?>!</div>
- 文字列操作によって生成されたSQLクエリの全事例(
mysql_query
について書かれたPHPのドキュメントを見てください)
$query = sprintf("SELECT firstname, lastname, address, age FROM friends
WHERE firstname='%s' AND lastname='%s'",
mysql_real_escape_string($firstname),
mysql_real_escape_string($lastname));
- innerHTML的なやり方を使って、多くのプログラマが生成するJavaScriptにおける動的な要素の 例 :
var OriginalContent = $(this).text();
$(this).html("<input type='text' value='" + OriginalContent + "' />");
- 未熟なプログラマがC言語で書いた動的なWebサイト:
sprintf(buffer, "<tr><td onclick=putpgtl(\"?j=%d&k=%d&v=%d&h=24\")>%s24時間</td></tr>", ...)
- 文字列フォーマットを使ったJSONの生成: gist.github.com/varemenos/e95c2e098e657c7688fd
git log --pretty=format:'{ %n "commit": "%H",%n "abbreviated_commit": "%h",%n "tree": "%T",%n "abbreviated_tree": "%t",%n "parent": "%P",%n "abbreviated_parent": "%p",%n "refs": "%D",%n "encoding": "%e",%n "subject": "%s",%n "sanitized_subject_line": "%f",%n "body": "%b",%n "commit_notes": "%N",%n "verification_flag": "%G?",%n "signer": "%GS",%n "signer_key": "%GK",%n "author": { %n "name": "%aN",%n "email": "%aE",%n "date": "%aD"%n },%n "commiter": { %n "name": "%cN",%n "email": "%cE",%n "date": "%cD"%n }%n},'
# jesus christ why
(curl json) | jq -c '.[] | {value: .value, name: .name}' | sed -e 's/"name":"//g' -e 's/","value"//g' | tr -d '"}' | grep -v ':0' | awk '{FS=":" ; printf "%20s\t\%d\n",$1,$2}' | less
- XMLの処理に
sed
を使用する
http://askubuntu.com/questions/442013/using-sed-to-search-and-replace-text-in-xml-file
sed -i 's##<UpdateAccountGUIDs>UpdateAndExit</UpdateAccountGUIDs>#' File.XML
# http://askubuntu.com/questions/284983/print-text-between-two-xml-tags
sed -n '/<serverName/,/<\/serverName/p' big_xml_file.xml
'<tpl if="hasCustomConvert">',
' dest["{name}"] = value === undefined ? __field{#}.convert(__field{#}.defaultValue, record) : __field{#}.convert(value, record);\n',
...
// exploited using
// Ext.define('m',{extend:'Ext.data.Model',fields:['id']});
// var store = Ext.create('Ext.data.Store',{model:m});
// store.loadRawData({metaData:{fields:['"+alert(1)+"']}});
- malでは、
load-file
を連結された文字列に対するeval
として 定義 している
mal自体を使って
load-file
関数を定義してください。主プログラムにおいて以下の文字列とともにrep
関数をコールしてください。"(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))"
適切な選択肢
それでは、文字列関数を使ってHTMLを生成している場合、代わりにどうすればいいのでしょうか?
提案された解決策の全てに共通のテーマがあることが分かるでしょう。基となるデータ構造を操作し、シリアライズするのです。
シリアライズされたデータ構造を文字列として変更するのは無駄なことです。
HTMLの場合
(html [:ul
(for [x (range 1 4)]
[:li x])])
(defn index []
[:div {:id "content"}
[:h1 {:class "text-success"} "Hello Hiccup"]])
- lxml e-factory
html = page = (
E.html( # create an Element called "html"
E.head(
E.title("This is a sample document")
),
E.body(
E.h1("Hello!", CLASS("title")),
E.p("This is a paragraph with ", E.b("bold"), " text in it!"),
E.p("This is another paragraph, with a", "\n ",
E.a("link", href="http://www.python.org"), "."),
E.p("Here are some reservered characters: <spam&egg>."),
)
)
)
- DOMを操作し、HTMLをシリアライズ
お遊びの例ですが、以下のようになります。
html = etree.parse('template.html')
name_node = html.xpath('//div[@id="user-name"]')[0]
name_node.text = user.name
print(etree.tostring(html))
-
サーバーサイドAngularJSまたはReact
-
DOMを直接操作
以下はjQueryの例です。
var OriginalContent = $(this).text();
$(this).empty().append($('<input type=text>').val(OriginalContent))
// Bad code. BAD!
// $(this).html("<input type='text' value='" + OriginalContent + "' />");
JSONの場合
- オブジェクトリテラルかリスト内包表記を使ってJSONオブジェクトを作成し、シリアライズ
- JSONオブジェクトを生成してシリアライズ
SQLの場合
- クエリのプレースホルダを使用
- SQL抽象構文木を操作して、SQLを出力
- LINQ to SQL
XMLの場合
- 代わりにまともなシリアライズフォーマットを使用(JSONを参照)
- DOMを操作し、XMLをシリアライズ
- XSLTは、ひどい状態なので使えない
メタプログラミングの場合
- Lisp
- 使用言語の抽象構文木を操作
理由
なぜこうなったのでしょうか。
人々が怠惰なせいで起こったのだと私は疑っています。
— へえ、文字列連結を使って簡単に生成できるマークアップがあるんですね。じゃあ、そうしましょう。
— でも、
<img/src/onerror=alert(1)>
を含む文字列があったら、JavaScriptのコードが実行されてしまいます。— うーん、
htmlspecialchars
> 関数を書いて、文字列をHTMLに入れる度に、この関数を介して文字列を渡すようにしてみましょう。— しかし、
"<img class=" + htmlspecialchars($_GET['cls']) + " src=dot.gif>"
> を書くと、まだJavaScriptを入れることができてしまいます。— だが、そんなコードを書くのはバカだけです。
— そもそもコードが無効なHTMLを出力する場合、文字列を置換せずに、どうすればいいのでしょうか。
— とにかく次回は注意してコードを書いてください。
そういうわけで、文字列の連結を用いてHTMLを生成してしまっているのです。
もちろん、 本当に 細心の注意を払っていれば、インジェクションが問題になることはなく有効なHTMLやSQLを生成できます。
しかし、Webサイトを作成する際に、手動のメモリ管理やポインタ演算は使いませんよね?
安全にプログラミングできるなら、あえて危険な方法を取る必要はありません。
ブラウザ
もちろん、この話にも面白い部分があります。ブラウザです。
多くのWebサイトが無効なHTMLをブラウザに表示させようとしたため、ブラウザベンダは正しくないHTMLにもパーサを適合させる必要がありました。人々は、自分のお気に入りのWebサイトが表示されるように、ほぼ任意のバイトの塊をHTMLとして処理できるブラウザを選びました。
Webサイトは、ユーザのリクエストをそのまま直接HTMLに含めてしまうことが多いので、ブラウザベンダはXSSフィルタを実装する必要がありました。XSSフィルタを用いる根拠は簡単です。ブラウザベンダが、ユーザに対するXSS攻撃の90%を 防げる なら、ユーザはうれしいですよね。
しかしXSSフィルタは、すべてのXSS攻撃を単純に防ぐことは できません 。
これら2つの例では、ブラウザが問題から引き起こされる症状を扱っていますが、問題そのものに対処していないのです。問題は、文字列を操作して動的なHTMLを生成することが合理的だと思うプログラマの考え方にあります。
結論
かなり初期段階からHTMLが文字列として操作されるようになったため、HTMLは、構造化されたデータフォーマットとして、ひどい状態です。このように、HTMLフォーマットを誤った方法で扱うことで、多くの問題が発生しています(XSSや無効なHTML、ブラウザのパーサの違いだけでなく、他の問題も含まれます)。
おそらく、あくまで推測ですが、利用できるツールがHTMLを文字列として生成するように促さなければ、Webの世界はもっとマシな環境だったでしょう。
おそらく、あくまで推測ですが、Webのドキュメントについて、異なるシリアライズのフォーマットを選んでいたら、HTMLをprintfで書ける文字列として扱うことはなかったでしょう。
また、もしプログラマが、文字列関数を使って構造化されたデータフォーマットを構築することを許容しなければ、脆弱性は確実に減らせていたでしょう。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa