POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

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

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

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。