Skip to main content

DjangoでXSSを防ぐ方法

著者
blog feature django xss

2023年3月13日

0 分で読めます

クロスサイトスクリプティング(XSS)は、Webアプリケーション上でのユーザー操作を悪用し、ユーザーのブラウザー環境を侵害する脆弱性です。Djangoなどの最新フレームワークで構築されたものを含め、多くのWebアプリがこの脆弱性の影響を受ける可能性があります。

XSS攻撃は非常に広く行われているため、アプリケーションを攻撃から守ることが欠かせません。このガイドでは、DjangoアプリでXSS脆弱性がどのように発生するのか、そしてそのリスクを軽減する方法を解説します。また、無料のセキュリティツールを使って、開発の早い段階でXSS脆弱性を検出・修正する方法も紹介します。

XSSとは?

XSSという名称は、攻撃者が主にクロスサイトのデータ窃取を狙っていた攻撃初期に由来します。しかし、XSS攻撃は進化し、現在ではクライアント側のデータやトークンなどを攻撃者が侵害できるあらゆる攻撃を指すと考えられています。攻撃が成功すると、セッションハイジャックからアカウントやシステムの完全な乗っ取りまで、さまざまな被害につながる可能性があります。

XSS攻撃では、検証されていない不正なデータがアプリケーションに入り、ユーザーに返されて表示されます。レスポンスに含まれる悪意のあるデータは、多くの場合、ユーザーのブラウザーで実行できるJavaScriptなどのコードです。ブラウザーがこのコードを実行すると、同一オリジンポリシー(SOP)を回避してユーザーを攻撃します。

従来、XSS攻撃はデータの永続性に基づいて、リフレクティブ型と格納型の2種類に分類されてきました。また、DOMベースXSSと呼ばれる新しいタイプの攻撃もあります。

格納型XSS

格納型XSSは、永続型XSSとも呼ばれます。脆弱なアプリケーションのサーバーに悪意のあるコードが保存され、サニタイズされないままユーザーに送信されて(最終的にユーザーのブラウザーで実行されて)発生します。悪意のあるペイロードは、データベース、フォーラムの投稿、ログ、コメント欄などに保存されることがよくあります。

保存型XSS攻撃を示す図:攻撃者がWebサイトにペイロードを注入し、被害者がアクセスすると悪意のあるコードが送信され、被害者の情報が

被害者が影響を受けたアプリケーションにアクセスすると、悪意のあるペイロードがサーバーのレスポンスの一部としてブラウザーに表示されます。ブラウザーがこのデータを実行し、ユーザーが侵害されます。

リフレクティブ型XSS

リフレクティブ型XSSは、アプリケーションから返されるユーザー入力がサーバーに保存されない場合に発生します。この場合、信頼できないコードが、適切にサニタイズされないまま検索結果やエラーメッセージの一部として送信されます。

反射型XSS攻撃を示す図。攻撃者が悪意のあるリンクを送信し、Webサイトが被害者にコードを返すことで、情報が攻撃者に送信されます。

攻撃者は通常、偽装して被害者にペイロードを送信します。被害者がペイロードをクリックすると、悪意のあるコードが影響を受けたアプリケーションに送られます。アプリケーションはそのコードを含むレスポンスを返し、被害者のブラウザーで実行されます。

DOMベースXSS

DOMベースXSSは、悪意のあるデータの流れがブラウザーのDOM内で始まり、DOM内で完結するときに発生します。被害者のブラウザーのDOMを操作することで脆弱性が引き起こされます。HTTPレスポンス自体は変化せず、クライアント側のコードが返されるだけですが、そのコードがブラウザーのDOMを変更し、ペイロードを実行します。

DjangoでXSSを防ぐ方法

Djangoには、テンプレートエンジンの自動エスケープ機能など、XSS攻撃に対する組み込みの保護機能があります。ただし、一般的なXSS攻撃を防ぐことはできても、不適切なコーディング慣行に起因する攻撃経路までは防げません。

アプリの構築時にベストプラクティスに従うことで、XSS攻撃に対する脆弱性を最小限に抑えられます。

動的データの属性値は引用符で囲む

サニタイズされていないユーザー入力を受け付ける箇所がアプリにあると、XSS脆弱性につながる可能性があります。攻撃者はこうした入力フィールドを使い、HTML文字を必要としない任意のJavaScriptコードを挿入できます。

たとえば、次のHTMLでは属性値が引用符で囲まれていないため、onmouseover=alert(1)のようなJavaScriptのイベントハンドラーを挿入できます。

<div class={{ name }}></div>

もちろん、実際の攻撃者はこれほど無害なことはしません。この機会を最大限に利用するため、独自のペイロードを挿入します。引用符で囲まれていないペイロードに起因するXSS脆弱性の別の例として、JavaScript変数にユーザーデータを格納するケースがあります。

var user = new userClass();&nbsp; 
user.age = {{ age }} ;

このようにJavaScriptの整数型変数に入力値を格納すると、アプリがXSS攻撃にさらされます。Djangoの自動エスケープ機能ではこれを防げません。攻撃者は23 ; payload()のような単純な値を入力するだけで、ユーザーのブラウザー上で悪意のあるコードを実行できます。

ユーザーに表示する属性値をすべて二重引用符で囲めば、こうした脆弱性を軽減できます。

<div class="{{ name }}"></div>

テンプレートリテラルを避ける

コードにJavaScriptのテンプレートリテラルが含まれている場合、XSSの脆弱性がある可能性があります。テンプレートリテラルでは引用符の代わりにバッククォートを使い、次の文字をエスケープしません。

<img src="javascript:alert(`xss`)>

攻撃者はバッククォートを含むペイロードを作成することで、コードを実行できます。これを防ぐ最善の方法は、テンプレートリテラルの使用を禁止することです。

属性URLを検証する

a hrefやimg srcなど、多くのHTML属性ではURLが指定されますが、XSS攻撃に悪用される可能性があります。攻撃者はjavascript: URIを含むペイロードを挿入するだけで、コードを実行できます。このケースでは、DjangoのHTMLエスケープでは防御できません。

<a href="{{ user.website_url }}">Website</a>

たとえば、アプリでユーザーが個人サイトのURLを追加できるとします。悪意のあるユーザーは、リンクにjavascript:payload(XSS)のような値を指定できます。誰かがそのリンクをクリックすると、悪意のあるペイロードが実行されます。

この問題を解決するには、URLで許可するプロトコルのリストを作成し、それ以外を禁止します。安全なURIにはhttp:、https:、ftp:、mailto:などがあります。データベースに保存する前に属性URLをサニタイズする方法もあります。

JavaScriptデータをエスケープする

変数データをJavaScriptに直接埋め込むと、そのデータは実行コンテキストに入ります。JavaScriptブロックやイベントハンドラーに挿入されるデータをユーザーが制御できるプログラムでは、XSS攻撃につながる可能性があります。

<script>var name = {{ username }};</script>

攻撃者はHTML文字を使わずにコードを作成できるため、このようなケースではDjangoのHTMLエスケープに頼ってもXSSを防げません。

リスクを軽減するには、<script>ブロック内でのテンプレート変数の使用を禁止するか、JSデータの読み込みにjson_scriptテンプレートタグを使用します。

{{ name|json_script:"username" }}
<script>
  var name = JSON.parse(document.getElementById(‘username’).textContent);
</script>

データの文字を\xHH形式でエンコードし、JSONのContent-Typeヘッダーがapplication/jsonであり、text/htmlではないことも確認してください。

CSSデータをエスケープする

CSSスタイルタグや属性に挿入されたデータは、DjangoアプリのXSS脆弱性につながる可能性があります。攻撃者はこのCSSデータを利用して実行コンテキストに入り込みます。通常は、CSSコンテキストにJavaScriptコードを挿入して行われます。

<style> body{ margin-left:expression('alert(‘XSS’)') } </style>

expression()ディレクティブを使うと、CSSプロパティの値を評価するために任意のJavaScript文を使用できるため、DjangoアプリがXSS攻撃にさらされます。

リスクを軽減するには、CSSパラメーターの値の設定にexpression()ディレクティブを使用しないでください。JavaScript文でCSSプロパティを設定または変更する場合は、style.property = xのような方法を試してください。

safeの使用を最小限にする

Djangoの自動エスケープはほとんどの場合に有効ですが、テンプレート内でsafeフィルターを使ってHTMLエスケープを無効にすると保護されません。何かをsafeとして扱うと、Djangoはエスケープせず、そのままデータをレンダリングします。

<span id="query" >Searches similar to {{ query | safe }}</span>

データを検証せずにこのクエリをアプリでレンダリングすると、攻撃者にペイロードを送り込まれる可能性があります。データが本当に安全だと確信できる場合に限りsafeフィルターを使用してください。ユーザーが入力したデータをsafeとして扱ってはいけません。

safeseqフィルターの使用を制限する

safeseqフィルターは、レンダリングするコンテンツが完全に安全であることを示します。safeと同様に、XSSペイロードを招く可能性があります。

{{ ids | safeseq | join:", " }}

シーケンスデータにはこのフィルターを使用しないでください。どうしても使用する必要がある場合は、mark_safe()を使ってください。コードレビュー時に、安全であると明示的に指定した箇所を確認できます。

html_safe()の使用を制限する

html_safe()メソッドは、指定されたクラスに__html__マジックメソッドを追加し、そのクラスの文字列表現をそのまま返します。

@html_safe
class UserData(str):
  pass

このデータはDjangoのテンプレートエンジンでエスケープされないため、XSSの原因となる可能性があります。html_safe()は必要な場合を除いて使用しないでください。また、クラス内で__html__マジックメソッドを使用することも避けてください。

is_safe=Trueを使ったフィルターを避ける

is_safe=Trueを指定してカスタムフィルターを登録すると、DjangoはHTMLをエスケープせず、フィルターの戻り値を安全なものとして扱います。

@register.filter(is_safe=True)
def randomfilter(value):
  return value

このフィルターの戻り値はDjangoの自動エスケープを通らないため、XSS攻撃につながる可能性があります。アプリケーションの安全性を確保するため、戻り値が安全に使えると確信できる場合を除き、この方法でフィルターを登録しないでください。

mark_safe()とSafeStringの使用を制限する

mark_safe()メソッドはレンダリングするデータを安全なものとして扱い、Djangoに組み込まれたXSS保護を回避します。このメソッドを頻繁に使うと、XSS脆弱性の温床になりかねません。

mark_safe(some_content)

さらに、mark_safe()を使うと、データはSafeStringとして返されます。DjangoはSafeStringクラスを使って、どのデータが安全にレンダリングできるかを判断します。では、SafeStringを直接使うとどうなるでしょうか?

SafeString(f"<div>{request.POST.get('id')}</div>")

お察しのとおり、このようにSafeStringクラスを直接使用すると、DjangoのHTMLエスケープを回避し、XSSにつながる可能性があります。mark_safe()とSafeStringのどちらも、使用を最小限に抑えるのが最善です。

HttpResponseを使ったレスポンスの作成を避ける

HttpResponseなどのクラスをコード内で直接使うと、Djangoのテンプレートシステムを回避し、HTMLエスケープが無効になります。

return HttpResponse("My name is, " + name)

これによりプログラムがXSS攻撃にさらされる可能性があるため、この方法でのレスポンス作成は避けてください。代わりに、テンプレートとrender()メソッドを使用できます。

SnykでDjangoのXSS脆弱性を検出・修正

XSS攻撃は、最新のWebアプリを狙う脅威の中でも特に活発なものの一つです。さまざまな形で発生するため、コードレビューですべてのXSS脆弱性を検出するのは困難です。そのため、ソフトウェア開発ライフサイクル(SDLC)の早い段階でこうした脆弱性を発見することが重要です。

開発環境に専用のテストツールを統合することで、早期発見を実現できます。ここではSnykを使って説明します。

Snykでは、Web UI、コマンドラインインターフェース、IDEプラグイン、APIなど、Djangoアプリをテストする方法をいくつか提供しています。このチュートリアルではSnyk PyCharmプラグインを使用します。手順に沿って進めるには、PyCharmがインストールされている必要があります(SnykはEclipseやVisual Studioなど、ほかのIDEにも対応しています)。

Snykを使うには、無料アカウントを作成してから、インストールガイドに従ってプラグインをインストールします。

「snyk」を検索したプラグインマーケットプレイスと、[インストール]ボタンが表示されたSnyk Securityプラグインを含む、PyCharmの設定ウィンドウ。

プラグインをインストールしたら、IDEでSnykプラグインを設定します。プラグインをSnykプラットフォームに接続するにはSnyk CLIが必要ですが、別途インストールする必要はありません。初めてスキャンを実行すると、Snykが自動的にダウンロードします。

PyCharmにDjangoのrequirements.txtファイルが表示され、SnykパネルでSnyk CLIをダウンロードしている。

Snykの認証を求められたら、Test code nowをクリックすると、Snyk Web UIが開きます。

Snykパネルを開いてコードのセキュリティテストを行っている、Djangoのbase.htmlテンプレートを表示したJetBrains IDE。

Authenticateをクリックして、プラグインの連携を確認します。

「CLIで認証してください」というメッセージと[認証]ボタンが表示されたSnykのWebページ

認証が完了すると、Snykは自動的にDjangoアプリの解析を開始し、IDEのSnykタブに問題を表示します。また、Run scanをクリックして解析を開始することもできます。

コードエディターで開かれたDjangoプロジェクト。Snykのパネルに、セキュリティ脆弱性とコードの問題をスキャンするよう促すメッセージが表示されている。

Snykは、検出された脆弱性の深刻度や推奨される修正方法など、役立つ情報を表示します。

ハードコードされたSECRET_KEYを含むDjangoのsettings.pyファイルと、重大度「高」のハードコードされたシークレットの脆弱性を報告するSnykを表示したIDE。

Snykが推奨するアップグレードに従って、脆弱性を修正できます。プラグインは常に、コードの修正に必要な最小限の変更を提案します。

まとめ

XSS攻撃は、Djangoアプリケーションで最もよく見られるセキュリティ問題の一つです。発生源が多岐にわたり、さまざまな形を取るため、完全に防ぐのは困難です。しかし、これらのベストプラクティスに従うことで、XSSが発生するリスクを低減できます。

開発中に適切なツールを使うことも重要です。Snykのような専用のセキュリティプラットフォームを活用すれば、アプリの開発の早い段階でXSSの脆弱性を検出し、修正できます。

Capture the Flagを始めよう

オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。