最も割高なアンチパターン : 構造化されたデータを文字列関数で操作する「printfアンチパターン」について

本記事では、私の知る最も割高なアンチパターンとなるプログラミングについて述べます。

それは、構造化されたデータフォーマットを文字列関数を使って操作することです。

以後これを”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>", ...)
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"]])
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で書ける文字列として扱うことはなかったでしょう。

また、もしプログラマが、文字列関数を使って構造化されたデータフォーマットを構築することを許容しなければ、脆弱性は確実に減らせていたでしょう。