Upload
gouji-ochiai
View
1.385
Download
6
Embed Size (px)
DESCRIPTION
Basics of Web Secure Coding on Python
Citation preview
Webセキュアコーディングの基本
on 2013-09-15 13:00-13:50JSTat PyCON APAC 2013 Room A0765 (Ja2)
by OCHIAI, Gouji (@gjo)
Abstract
• セキュアコーディングとは何か?
• Webシステムにおけるセキュリティ
• コード解説中心
• 基本的な内容しかやりません
• 「お前が言うな」は重々承知
お前誰よ?• 落合 豪史
@gjo
• 一応職業プログラマーセキュリティの専門家とかではないです
• twitter.com/gjogithub.com/gjo
本セッションおよび本稿の内容は私個人の見解であり、私の所属する企業、団体等とは一切の関わりはありません。
The Year of Python
Year of the Dragon
心を入れ替えるよその方法を教えてくれ
セキュアコーディングとは何か?
• 脆弱性に対して堅固なコーディング
• 脆弱性?
• 堅固?
http://www.flickr.com/photos/nene9/4444657449/
セキュアな状態?• 一般的にセキュリティは非機能要件
• 「セキュリティが保たれていること」
• 実装に依存する要件
• 実装に依存しない要件
http://www.flickr.com/photos/jermainejustice/3352100979/
実装に依存しないセキュリティ要件
• 「必要でない情報」を
• 「必要でない対象」に
• 「流通しない」
実装に依存しないセキュリティ要件 (Cont.)
• 「必要な情報」を
• 「必要な対象」に
• 「流通する」
• 一般的な機能要件の逆
相互作用を持たないアクターにユースケースが提供されないこと
情報?対象?流通?• 概念データモデリングの段階である程度明らかになっているはず
• システムの外側に何をみせるか?
• プログラムは何を永続化するか?• 平文のパスワードとか• クレジットカードのセキュリティコードとか
実装に依存するセキュリティ要件
• 攻撃手法が刻々編み出される
• 既知の攻撃方法について対処されている、ということしか証明できない
http://www.flickr.com/photos/danorth1/315489224/
脆弱性は時系列上で固定ではない
堅固?• 現時点で既知の脆弱性が存在しない、ではセキュアコーディング足り得ない
• 脆弱性のあるコードが存在しないことが自明な状態であることが、容易に測定出来ること
• 考えたら負け
考えたら負けDon’t Think...Feel!
考えたら負け
• 脆弱性有無の計測は何度も行われることになる
• その都度、考えなければ脆弱性有無を判定できないものは「堅固」ではない
安全性を型に嵌めるいちいち考えないために
このコードはセキュアですか?
https://gist.github.com/gjo/6473557
--- is_this_safe.py 2013-09-14 17:42:19.000000000 +0900+++ is_this_safe2.py 2013-09-13 21:07:16.000000000 +0900@@ -40,8 +40,12 @@ sql = 'SELECT {} FROM page'.format(','.join(COLUMNS)) params = [] if None not in (field, value):+ if field not in COLUMNS:+ raise NotFound(field) sql += ' WHERE {} LIKE ?'.format(field)- params.append(value)+ params.append('%{}%'.format(+ value.replace(r'\', r'\\).replace(r'%', r'\%').replace(r'_', r'\_'))+ ) cursor = db.cursor() cursor.execute(sql, params) pages = [dict(zip(COLUMNS, row)) for row in cursor]@@ -62,7 +66,7 @@ <tr><td>No maches. {% endfor %} </table>-""")+""", autoescape=True) return template.render(context)
if __name__ == '__main__':- app.run(debug=True)+ app.run()
プログラム内部に起因する脆弱性
• メモリ管理、ランタイム関係• バッファオーバーフロー• 不正なシステム特権取得など
• 自爆• デバッグモード、メンテナンスモード• エラーメッセージ
プログラムの外部インタフェースに起因する脆弱性
• HTTP Request, HTTP Response
• Database
• MessageQueue, Cache, SMTP
• ...etc
古式ゆかしき何とやら
• SQL Injection
• Directory TraversalOS Command Injection
• Response Header InjectionResponse Header SplitingE-mail Header Injection
SQL Injection
def some_view(req): sql = 'SELECT * FROM {}'.format( req.GET['some_param'], ) cursor = req.db.cursor() cursor.execute(sql) for row in cursor: some_action(row)
Directory Traversal
def some_view(req, filename): path = os.path.join(ROOT, filename) content = open(path).read() resp = Response( content=content, content_type='', ) return resp
OS Command Injection
def some_view(req, cmdopt): resp = Response(content_type='') subprocess.call( 'some_command ' + cmdopt, shell=True, stdout=resp, stderr=subprocess.STDOUT, ) return resp
Response Header InjectionResponse Header Spliting
HTTP/1.0 200 OKContent-Type: text/htmlDate: Sun, 15 Sep 2013 04:00:00 GMTSet-Cookie: HOGEHOGE; expires=Sun, 15-Jul-2013 15:00:00 GMTConnection: Close
<html>...</html>
E-Mail Header Injection
Date: Sun, 15 Sep 2013 04:00:00 GMTFrom: "FOO" <[email protected]>To: "BAR" <[email protected]>Subject: Hello BAR: Thank you for your registration
Hi all,foobarbaz.
共通する危険性• テキストベースのプロトコル• 構造が存在する
• 構造を実現するためにメタキャラクタが存在する
• メタキャラクタを使用することで、誤動作を誘発する
解法
• 入力のバリデーション
• 出力のエスケープ
入力のバリデーション• "すべての"入力を検査すべき
• QueryString, FormDataはライブラリが充実している
• それゆえPATH_INFOを忘れがち
• Cookie他ヘッダーも忘れがち
出力のエスケープ• 出力先毎に異なるエスケープルール
• HTTPレスポンス
• ヘッダー: Cookie, Content-Type, Location etc...
• 本文: HTML, JSON/JSONP, CSV, etc...
• インタフェース
• 永続化等: データベース, ファイルシステム, キャッシュ
• 外部接続: SMTP, Web-API Request, etc...
なぜ両方必要か?
AppINPUT OUTPUTValidate Escape
入力をAppで使用できる形式に(Byte strem to Python type)
OUTPUTをプロトコルに合わせる(Python type to Byte strem)
つまらない結論を
フレームワークのルールに従ってコーディングしましょう
なぜ?
• ひとつの問題を解決する手法はひとつであるべき
• 同じことを何度も書かない
どこかで見たような
•Zen of Python
•DRY原則
チュートリアルの罠
• https://gist.github.com/gjo/6558690
• https://gist.github.com/gjo/6558691
# -*- coding: utf-8 -*-# http://flask.pocoo.org/docs/quickstart/#a-minimal-application# から# http://flask.pocoo.org/docs/quickstart/#variable-rules# までを写経していくと
from flask import Flask
app = Flask(__name__)
@app.route('/')def index(): return 'Index Page'
@app.route('/hello')def hello(): return 'Hello World'
@app.route('/user/<username>')def show_user_profile(username): # show the user profile for that user return 'User %s' % username
if __name__ == '__main__': app.run()
# -*- coding: utf-8 -*-# http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/# から抜粋
from wsgiref.simple_server import make_serverfrom pyramid.config import Configuratorfrom pyramid.response import Response
def hello_world(request): return Response('Hello %(name)s!' % request.matchdict)
if __name__ == '__main__': config = Configurator() config.add_route('hello', '/hello/{name}') config.add_view(hello_world, route_name='hello') app = config.make_wsgi_app() server = make_server('0.0.0.0', 8080, app) server.serve_forever()
フレームワークの選択• 必要な外部インタフェースが実装されているか?
• フレームワークへのアドオン追加(新規作成 or グルーコードのみ作成)
• 個別のコードでバリデートやエスケープを実装しない
では本題
Against SQL Injection
• SQLを動的に組み立てるなORM使え
• どうしても必要であれば、置換箇所は完全一致で確認して、特殊文字を追い出せ
• Prepared Statementの場合も、値の箇所で評価される特殊文字はハンドルしろ
SQL LIKE or REGEX
# LIKEのエスケープはORMに任せるquery.filter(MyModel.myfield.endswith( myparam,))
# REGEXのエスケープはまともに書ける# レベルを超えてしまう。。。
Against Directory Traversal
def safe_path(*args): return os.path.join( [PRE_DEFINED_ROOT] + [os.path.basename(p) for p in args])
def some_view(req, module_, file_): path = safe_path(module_, file_)
Response Header InjectionResponse Header Spliting
• WSGIサーバーでのResponseHeaderの扱いは単なるByte (!Unicode)
http://www.python.org/dev/peps/pep-3333/#the-start-response-callable
http://www.python.org/dev/peps/pep-3333/#unicode-issues
• フレームワークごとで動作が違う
• django (convert)https://docs.djangoproject.com/en/1.5/ref/request-response/#setting-headers
• flask (Exception on NL)http://flask.pocoo.org/docs/quickstart/#about-responses
• pyramid (Exception on NL in Key)http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/api/response.html#pyramid.response.Response.headers
• Cookieはたいてい特別扱いだが、動作そのものは、通常のヘッダの場合とほぼ同じ
• djangohttps://docs.djangoproject.com/en/1.5/ref/request-response/#django.http.HttpResponse.set_cookie
• flaskhttp://flask.pocoo.org/docs/quickstart/#cookies
• pyramidhttp://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/api/response.html#pyramid.response.Response.set_cookie
E-mail Header Injection
• 標準ライブラリemailでも問題なくエスケープを捌けるhttp://docs.python.jp/2/library/email.header.html#email.header.Header
(おまけ)
流通しない情報は取得しない
Script Injection大事なものを忘れていないかい?
Script Injection
<html>{{ unchecked_var|safe }}</html>
Script Injection
<script>var x = {{ unchecked }};</script>
Script Injection
<style>.some-class { color: {{ unchecked }};}</style>
Script Injection
<script src="{{ unchecked }}"></script>
スクリプト実行が問題になるのは何故か
• DOMを操作できる
• 新しくHTTP Requestを発生させられる
• iframe, img, link, script
• イベントを発生させられる• a.click, form.submit
Some Server Attack Servers
Browser
Some Document
Attack Documents
何が問題か
• 同意していないリクエストが送信される
• パーソナライズされている情報が外部に送信される
問題が複雑になってしまう背景
• クライアントはサーバーを信用してはならない• クライアントはサーバーを信用せざるを得ない• サーバーはクライアントを信用してはならない• サーバーはクライアントを信用せざるを得ない
クライアントはサーバーを信用してはならない
• 正しいサーバーに接続しているのか
• サーバーに送信した情報は妥当に取り扱われているのか
• サーバーから受信した情報は信用に足りるのか?
クライアントはサーバーを信用せざるを得ない
• ひとまず正しいサーバーに接続していると仮定するに足る手法を使う
• ひとまずプライバシーポリシー等のデータハンドリングを信じる
• ひとまずレスポンスは正しいと信じる
サーバーはクライアントを信用してはならない
• 接続元クライアントが正しいユーザーであるか見分けることは困難
• クライアントから受信した情報が妥当であるか保証できない
• クライアントに送信した情報が適切に扱われるか保証できない
サーバーはクライアントを信用せざるを得ない
• ひとまず接続元が悪意あるユーザーでないことを証明するに足るチェックを行う
• ひとまずクライアントから受信した入力に虚偽はないと信じるに足るチェックを行う
• ひとまずレスポンスデータが想定しない外部に流出していないことを信じる
送信してしまったデータの来方行末を保証するものはない• クライアントはサーバーのセキュリティが保たれていることを信じるしかない
• サーバーはクライアントのセキュリティが保たれていることを信じるしかない
Same Origin PolicyThe Great Wall
Same Origin Policy
• “scheme://host:port” の組み合わせが一致するものを信じる
• Ajax Requestの範囲を制限する
• (類似の話題: 1st-party Cookie)
[横道] Cookie
• 適切な範囲のCookieかdomain, path, secure, httpOnly, expire
• (セッション値がhttpOnlyでないのは...)
• 3rd-Party Cookie
• DNT, EU Cookie法
Browsing Context
• (超省略形) JavaScriptが、どのページで動作するか
• 通常は表示しているページ• script要素で読み込んだ外部Originの
JavaScriptも、表示しているページのContextで動作する
Browsing Context (Cont.)
• iframe配下のdocumentは別のContext
• Context間でのデータ流通にもSame
Origin Policyが適用される
本当に信じていいの?• Ajax Requestにしか作用しない
• iframe, img, link, script要素の挿入によるRequestは送信される
• hostsファイル汚染/DNS汚染
• 中間のネットワーク汚染 [New!]
[横道] Content Security
Policy• HTTP Response Headerで指定
• サーバーが返却するリソースに何を使えるかを厳密に指定できる
• 対応ブラウザーでないと効果なし(Firefox-23, Chrome-25, IE-10 (experimental))
モダンな何とやら
• XSS (cross site scripting)
• CSRF (cross site request forgery)
• Click-jacking
• Mitigation, BEAST, CRIME, BREACH
XSS• Script injection等によって攻撃用のJavaScriptをscript
要素で挿入する
• 挿入手段による多くのバリエーション
• HTTP Response Header Spliting
• Response Content-Type誤認バグの利用
• script要素の代わりにimg要素やlink要素を利用
• とにかく意図しないScript実行を抑止するしかない
CSRF
• 直接POST Requestを送りつける
• 通常はXSSとの併用で踏み台ページを経由する
• すべてのFormにtokenパラメータを付与し、同時にCookieやSessionでもtokenを保持してPOST
受付時に、外部から割り込んだPOSTを抑止する
Click-jacking
• iframe等を利用して攻撃対象ページを操作する
• 通常はSame Origin Policyによって別ContextのDocumentは操作できない
• 攻撃対象ページにXSS脆弱性があると、踏み台ページのSameOriginでiframeが作成されやりたい放題に
BEAST, CRIME, BREACH
• SSL保護下にあるデータを総当り的な計算を使用して盗む
• ルーター等でのパケット観測が必要(カンファレンスの無線LANとか)
• 総当り的計算を行うためのRequestをXSSに生成する
モダンな何とやら (再訪)
• モダンな攻撃の多くはXSSとの組み合わせ
• HTTP Response以外のインタフェースへのエスケープは多くの場合対処済み
• HTTP Response以外では攻撃するためのシーケンスが長くなりがち
• 今更SQL injectionとか恥ずかしいだけ
まとめ• 日々新しい攻撃方法を考えている人がいます
• 多くは既知の攻撃方法の巧妙な組み合わせにエッセンスを加えたものです
• セキュアであるということは、時系列におけるスナップショットです
• よって何度も確認するハメになります
間違えようのないやり方がひとつだけあるのがいいね
― Zen of Python
EOF