質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
phpMyAdmin

phpMyAdminはオープンソースで、PHPで書かれたウェブベースのMySQL管理ツールのことです。

WordPress

WordPressは、PHPで開発されているオープンソースのブログソフトウェアです。データベース管理システムにはMySQLを用いています。フリーのブログソフトウェアの中では最も人気が高く、PHPとHTMLを使って簡単にテンプレートをカスタマイズすることができます。

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

CSRF

クロスサイトリクエストフォージェリ (Cross site request forgeries、CSRF)は、 外部Webページから、HTTPリクエストによって、 Webサイトの機能の一部が実行されてしまうWWWにおける攻撃手法です。

Q&A

解決済

1回答

717閲覧

Javascriptを使う際にCSRFの対策が必要なのかアドバイスお願い致します。

homepage-site

総合スコア28

phpMyAdmin

phpMyAdminはオープンソースで、PHPで書かれたウェブベースのMySQL管理ツールのことです。

WordPress

WordPressは、PHPで開発されているオープンソースのブログソフトウェアです。データベース管理システムにはMySQLを用いています。フリーのブログソフトウェアの中では最も人気が高く、PHPとHTMLを使って簡単にテンプレートをカスタマイズすることができます。

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

CSRF

クロスサイトリクエストフォージェリ (Cross site request forgeries、CSRF)は、 外部Webページから、HTTPリクエストによって、 Webサイトの機能の一部が実行されてしまうWWWにおける攻撃手法です。

0グッド

2クリップ

投稿2023/08/31 16:15

実現したいこと

セキュリティ上 安全なQ&A掲示板を作成したい

前提

PHPとデータベースを使い名前・コメント・アップロードファイル・スタンプが送信できる、Q&A掲示板を作成しております。
PHP側ではCSRF対策にトークンを実装しております。

single-index.php(インデックス)
single-input.php(入力画面)
single-refication.php(文字列チェック)

single-index.php

<?php session_start(); header('X-FRAME-OPTIONS: SAMEORIGIN'); class MAX_LENGTH { public const NAME = 50; public const MESSAGE = 500; } $noindexaccess = true; $errors = []; $mode = $_POST['mode'] ?? 'init'; switch ($mode) { case 'regist': $namae = $_SESSION['namae']; $message = $_SESSION['message']; $stamp = $_SESSION['stamp']; check(); break; case 'confirm': $namae = $_POST['namae']; $message = $_POST['message']; $stamp = $_POST['stamp']; check(); break; case 'input': $namae = $_SESSION['namae']; $message = $_SESSION['message']; $stamp = $_SESSION['stamp']; break; default: $namae = ''; $message = ''; $stamp = '1'; $_SESSION = []; break; } if ('init' !== $mode && empty($errors)) { $_SESSION['namae'] = $namae; $_SESSION['message'] = $message; $_SESSION['stamp'] = $stamp; } else { $mode = ''; } switch ($mode) { case 'regist': include 'single-regist.php'; break; case 'confirm': include 'single-confirm.php'; break; default: include 'single-input.php'; break; } function check() { global $errors; global $namae; global $message; $namae = Chk_StrMode($namae); $message = Chk_StrMode($message); Chk_InputMode($namae, '・お名前をご記入ください。'); Chk_InputMode($message, '・お問い合わせ内容をご記入ください。'); if (!empty($_FILES)) { foreach ($_FILES['attach']['tmp_name'] as $i => $tmp_name) { if (empty($tmp_name)) { if (empty($_POST['attachdel'][$i])) { if (empty($_SESSION['attach']['data'][$i])) { $_SESSION['attach']['data'][$i] = ''; $_SESSION['attach']['type'][$i] = ''; } } else { $_SESSION['attach']['data'][$i] = ''; $_SESSION['attach']['type'][$i] = ''; } } else { if (false) { $errors[] = 'ファイルエラー'.$i; } else { $data = file_get_contents($tmp_name); $_SESSION['attach']['data'][$i] = $data; $_SESSION['attach']['type'][$i] = $_FILES['attach']['type'][$i]; } } } } } function Chk_StrMode($str) { // タグを除去 $str = strip_tags($str); // 空白を除去 $str = mb_ereg_replace('^( ){0,}', '', $str); $str = mb_ereg_replace('( ){0,}$', '', $str); $str = trim($str); // 特殊文字を HTML エンティティに変換する $str = htmlspecialchars($str); return $str; } /* 未入力チェックファンクション */ function Chk_InputMode($str, $mes) { global $errors; if ('' == $str) { $errors[] = $mes; } } ?>

single-input.php

<?php if (!$noindexaccess) { exit('不正アクセス'); } if (empty($_SESSION['token'])) {// 悪意のある攻撃者があらかじめ作成したコードが実行されてしまうのを防ぐ $_SESSION['token'] = bin2hex(random_bytes(16)); } $attach = []; if (!empty($_SESSION['attach'])) { foreach ($_SESSION['attach']['data'] as $i => $data) { if (!empty($data)) { $base64 = base64_encode($data); } $type = $_SESSION['attach']['type'][$i]; switch ($type) { case 'image/jpeg': case 'image/png': $attach[] = '<img style="height: 100px;" src="data:'.$type.';base64,'.$base64.'">'; break; case 'video/mp4': $attach[] = '<video style="height: 100px;" controls src="data:'.$type.';base64,'.$base64.'">'; break; default: $attach[] = ''; break; } } } $stamp_checked = []; $stamp_checked[$stamp] = 'checked'; $upload_dir = wp_upload_dir(); $camera_url = $upload_dir['baseurl'].'/camera.png'; ?> <h2>入力画面</h2> <?php foreach ($errors as $error) { echo "<p>{$error}</p>"; } ?> <div class="board_form_partial" id="js_board_form_partial"> <form method="post" enctype="multipart/form-data"> <input type="hidden" name="token" value="<?php echo $_SESSION['token']; ?>"> <div class="image-partial"> <h2>動画・画像をアップロード(Upload video・image)<span class="required">※ファイルサイズ15MB以内、JPG/PNG/MP4</span></h2> <div class="image-selector-button"> <label> <div class="image-camera-icon"> <img src="<?php echo $camera_url; ?>" class="changeImg"> </div> <input type="file" class="attach" name="attach[]" accept=".png, .jpg, .jpeg, .mp4" style="display: none;"> </label> <input type="hidden" class="attachdel" name="attachdel[]"> <div class="viewer"><?php echo $attach[0]; ?></div> <button type="button" class="attachclear">clear</button> </div> <div class="image-selector-button"> <label> <div class="image-camera-icon"> <img src="<?php echo $camera_url; ?>" class="changeImg"> </div> <input type="file" class="attach" name="attach[]" accept=".png, .jpg, .jpeg, .mp4" style="display: none;"> </label> <input type="hidden" class="attachdel" name="attachdel[]"> <div class="viewer"><?php echo $attach[1]; ?></div> <button type="button" class="attachclear">clear</button> </div> <div class="image-selector-button"> <label> <div class="image-camera-icon"> <img src="<?php echo $camera_url; ?>" class="changeImg"> </div> <input type="file" class="attach" name="attach[]" accept=".png, .jpg, .jpeg, .mp4" style="display: none;"> </label> <input type="hidden" class="attachdel" name="attachdel[]"> <div class="viewer"><?php echo $attach[2]; ?></div> <button type="button" class="attachclear">clear</button> </div> </div> <style> .hideItems { display: none; } </style> <div class="title-partial"> <h2>名前<span class="required"></span></h2> <input class="length_input" data-maxlength="<?php echo MAX_LENGTH::NAME; ?>" type="text" name="namae" id="name" placeholder="未入力の場合は、匿名で表示されます" value="<?php echo $namae; ?>"> <div class="msg_partial"></div> </div> <div class="body-partial"> <h2>コメント<span class="required"></span></h2> <textarea class="length_input" data-maxlength="<?php echo MAX_LENGTH::MESSAGE; ?>" name="message" id="message" placeholder="荒らし行為や誹謗中傷や著作権の侵害はご遠慮ください"><?php echo $message; ?></textarea> <div class="msg_partial"></div> </div> <div class="stamp-partial"> <h2>スタンプを選ぶ(必須)</h2> <input type="radio" name="stamp" value="1" id="stamp_1" <?php echo $stamp_checked['1']; ?>><label for="stamp_1"></label> <input type="radio" name="stamp" value="2" id="stamp_2" <?php echo $stamp_checked['2']; ?>><label for="stamp_2"></label> <input type="radio" name="stamp" value="3" id="stamp_3" <?php echo $stamp_checked['3']; ?>><label for="stamp_3"></label> <input type="radio" name="stamp" value="4" id="stamp_4" <?php echo $stamp_checked['4']; ?>><label for="stamp_4"></label> </div> <div class="post-button"> <button type="submit" id="submit_button" name="mode" value="confirm">表示画面へ進む</button> </div> </form> <script> /* 名前とメッセージの文字数チェック */ const length_input = document.querySelectorAll('.length_input'); const submit_button = document.getElementById('submit_button'); for (let i = 0; i < length_input.length; i++) { length_input[i].addEventListener('input', lengthCheck); let event = new Event("input"); length_input[i].dispatchEvent(event); } function lengthCheck() { const left = this.dataset.maxlength - this.value.length; if (left >= 0) { this.nextElementSibling.innerHTML = 'あと<strong>' + left + '</strong>文字'; this.dataset.submit_disabled = this.value.length === 0; } else { this.nextElementSibling.innerHTML = '<strong>' + -left + '</strong>文字超過しています'; this.dataset.submit_disabled = true; } let disabled = false; for (let i = 0; i < length_input.length; i++) { if (length_input[i].dataset.submit_disabled === "true") { disabled = true; } } submit_button.disabled = disabled; } /* カメラ画像をファイルアップロード時に非表示にする */ const attach = document.querySelectorAll('.attach'); const del = document.querySelectorAll('.attachdel'); const clear = document.querySelectorAll('.attachclear'); const viewer = document.querySelectorAll('.viewer'); const changeImg = document.querySelectorAll('.changeImg'); for (let i = 0; i < attach.length; i++) { attach[i].addEventListener('change', () => { if (attach[i].files[0].size > 15 * 1024 * 1024) { alert('ファイルサイズが 15MBバイトを超えています'); return; } del[i].value = ""; viewer[i].innerHTML = ""; if (attach[i].files.length !== 0) { const reader = new FileReader(); reader.onload = () => { var child = null; if (reader.result.indexOf("data:image/jpeg;base64,") === 0 || reader.result.indexOf("data:image/png;base64,") === 0) { child = document.createElement("img"); } else if (reader.result.indexOf("data:video/mp4;base64,") === 0) { child = document.createElement("video"); child.setAttribute("controls", null); } else { alert("対象外のファイルです"); alert(reader.result); attach[i].value = ""; } if (child !== null) { child.style.height = "350px"; child.style.width = "528px"; child.src = reader.result; viewer[i].appendChild(child); changeImg[i].classList.add('hideItems'); // もともとの画像を消す } }; reader.readAsDataURL(attach[i].files[0]); } }); clear[i].addEventListener('click', () => { attach[i].value = ""; del[i].value = "1"; viewer[i].innerHTML = ""; changeImg[i].classList.remove('hideItems'); }); } </script>

single-refication.php

<?php function Chk_StrMode($str) { // タグを除去 $str = strip_tags($str); // 空白を除去 $str = mb_ereg_replace('^( ){0,}', '', $str); $str = mb_ereg_replace('( ){0,}$', '', $str); $str = trim($str); // 特殊文字を HTML エンティティに変換する $str = htmlspecialchars($str); return $str; } function Chk_InputMode($str, $mes) { $errmes = ''; if ('' == $str) { $errmes .= "{$mes}<br>\n"; } return $errmes; } ?>

試したこと

PHPでのトークン実装してCSRF対策

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

a.com

2023/08/31 16:57

CSRF対策は不要です
maisumakun

2023/08/31 21:25

> Javascriptを使う際にCSRFの対策が必要なのか どの部分での、どのような操作についての話でしょうか?
homepage-site

2023/09/01 06:42

回答ありがとうございます、Javascriptでフォーム送信する場合CSRFの対策が必要だと書いてあったので、アップロードファイルにも必要ではないかと考えたのですが、文字入力のチェックをPHPでしている場合、必要ないのでしょうか?
guest

回答1

0

ベストアンサー

アップロードファイルにも必要ではないかと考えたのですが

そもそも、ご提示のJavaScriptではファイル内容の検証を行っているだけで、送信は行っていません(ブラウザのフォームでそのまま送信するだけです)。

すでにフォーム内にCSRF対策トークンが書かれており、このままで必要十分だと思われます。

投稿2023/09/01 07:02

maisumakun

総合スコア145184

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

homepage-site

2023/09/01 14:40

アドバイスありがとうございます。CSRF対策トークンについて質問があるのですが、現状のコードだと不完全だとアドバイスを頂きました。 現状のコードだと不十分ではないのでしょうか? ※アドバイス頂いたもの セッション情報は攻撃者によって取得済みとなっているので信頼できず、同様の理由でセッションから取得できる値を種したトークンも利用できません。 ※代わりに提案していただいた方法 初回アクセス時にJavaScriptでトークンを作成しグローバル変数に保存、同時にPOSTでサーバーに送信しサーバー側で保存、データ送信時にブラウザのトークンを付けてPOSTで送信、整合性チェック
maisumakun

2023/09/01 14:51 編集

> 初回アクセス時にJavaScriptでトークンを作成しグローバル変数に保存 こんなやり方をしてはいけません。 JavaScriptはユーザーのブラウザで実行されるものですので、「攻撃者がCSRFトークンを生成できる」という、攻撃対策として全く無意味なものとなります。 (「セッション情報は攻撃者によって取得済みとなっているので信頼できず」とは、どのような攻撃を想定したものでしょうか?)
homepage-site

2023/09/01 15:09

回答ありがとうございます、悪意あるサイトから正しいトークン(セッション情報)を含んだリクエストが発行されており、これだけでは見分けがつかないため別の方法をとるべきとのご指摘を頂きました。 1回だけ有効なワンタイムトークンにしていないのが不十分だということだと思われます。 ※現在のコード if (empty($_SESSION['token'])) {// 悪意のある攻撃者があらかじめ作成したコードが実行されてしまうのを防ぐ $_SESSION['token'] = bin2hex(random_bytes(16)); } ※openssl_random_pseudo_bytes()関数を使い乱数を使ったトークンを作成 https://webukatu.com/wordpress/blog/11993/ ※代わりに提案していただいた方法 その2 外部のワンタイムパスワードや画像データなどを1ユーザーのみに表示し、それをユーザーに追加で入力&送信させ、2つの正当性が確認できたタイミングで受理する。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問