Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/Enums/SpamBlockType.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ class SpamBlockType extends EnumsBase
const email = 'email';
const domain = 'domain';
const ip_address = 'ip_address';
const honeypot = 'honeypot';

// key/valueの連想配列
const enum = [
self::email => 'メールアドレス',
self::domain => 'ドメイン',
self::ip_address => 'IPアドレス',
self::honeypot => 'ハニーポット',
];
}
34 changes: 27 additions & 7 deletions app/Plugins/Manage/SpamManage/SpamManage.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,15 @@ public function store($request)
abort(403, '権限がありません。');
}

// ハニーポットの場合は値不要、それ以外は必須
$block_value_rules = $request->block_type === SpamBlockType::honeypot
? ['nullable', 'max:255']
: ['required', 'max:255'];

// 項目のエラーチェック
$validator = Validator::make($request->all(), [
'block_type' => ['required', 'in:' . implode(',', SpamBlockType::getMemberKeys())],
'block_value' => ['required', 'max:255'],
'block_value' => $block_value_rules,
'target_forms_id' => ['required_if:scope_type,form'],
], [
'target_forms_id.required_if' => '適用範囲で特定フォームを選択した場合、フォームを選択してください。',
Expand All @@ -129,12 +134,17 @@ public function store($request)
$target_id = $request->target_forms_id;
}

// ハニーポットの場合は値をnullにする
$block_value = $request->block_type === SpamBlockType::honeypot
? null
: $request->block_value;

// スパムリストの追加
SpamList::create([
'target_plugin_name' => 'forms',
'target_id' => $target_id,
'block_type' => $request->block_type,
'block_value' => $request->block_value,
'block_value' => $block_value,
'memo' => $request->memo,
]);

Expand Down Expand Up @@ -177,9 +187,17 @@ public function update($request, $id)
abort(403, '権限がありません。');
}

// スパムリストデータの呼び出し(バリデーション前に取得してblock_typeを参照)
$spam = SpamList::findOrFail($id);

// ハニーポットの場合は値不要、それ以外は必須
$block_value_rules = $spam->block_type === SpamBlockType::honeypot
? ['nullable', 'max:255']
: ['required', 'max:255'];

// 項目のエラーチェック
$validator = Validator::make($request->all(), [
'block_value' => ['required', 'max:255'],
'block_value' => $block_value_rules,
'target_forms_id' => ['required_if:scope_type,form'],
], [
'target_forms_id.required_if' => '適用範囲で特定フォームを選択した場合、フォームを選択してください。',
Expand All @@ -196,18 +214,20 @@ public function update($request, $id)
->withInput();
}

// スパムリストデータの呼び出し
$spam = SpamList::findOrFail($id);

// 適用範囲の処理
$target_id = null;
if ($request->scope_type === 'form' && $request->filled('target_forms_id')) {
$target_id = $request->target_forms_id;
}

// ハニーポットの場合は値をnullにする
$block_value = $spam->block_type === SpamBlockType::honeypot
? null
: $request->block_value;

// 更新
$spam->target_id = $target_id;
$spam->block_value = $request->block_value;
$spam->block_value = $block_value;
$spam->memo = $request->memo;
$spam->save();

Expand Down
141 changes: 139 additions & 2 deletions app/Plugins/User/Forms/FormsPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,9 @@ public function index($request, $page_id, $frame_id, $errors = null)
}
}

// ハニーポット設定確認
$has_honeypot = $this->hasHoneypot($form);

if ($form->form_mode == FormMode::form) {
// フォーム
return $this->view('forms', [
Expand All @@ -466,6 +469,7 @@ public function index($request, $page_id, $frame_id, $errors = null)
'forms_columns' => $forms_columns,
'forms_columns_id_select' => $forms_columns_id_select,
'errors' => $errors,
'has_honeypot' => $has_honeypot,
]);
} else {
// アンケート
Expand All @@ -475,6 +479,7 @@ public function index($request, $page_id, $frame_id, $errors = null)
'forms_columns' => $forms_columns,
'forms_columns_id_select' => $forms_columns_id_select,
'errors' => $errors,
'has_honeypot' => $has_honeypot,
]);
}
} else {
Expand Down Expand Up @@ -822,6 +827,16 @@ public function publicConfirm($request, $page_id, $frame_id, $id = null)
]);
}

// ハニーポットチェック
$honeypot_check = $this->checkHoneypot($request, $form);
if ($honeypot_check['blocked']) {
$this->recordHoneypotBlock($honeypot_check, $form->id);

return $this->commonView('error_messages', [
'error_messages' => [__('messages.honeypot_blocked')],
]);
}

// スパムフィルタリングチェック
$spam_check = $this->checkSpamFilter($request, $form);
if ($spam_check['blocked']) {
Expand Down Expand Up @@ -949,6 +964,9 @@ public function publicConfirm($request, $page_id, $frame_id, $id = null)
}
}

// ハニーポット設定確認
$has_honeypot = $this->hasHoneypot($form);

// 表示テンプレートを呼び出す
if ($form->form_mode == FormMode::form) {
// フォーム
Expand All @@ -958,6 +976,7 @@ public function publicConfirm($request, $page_id, $frame_id, $id = null)
'form' => $form,
'forms_columns' => $forms_columns,
'uploads' => $uploads,
'has_honeypot' => $has_honeypot,
]);
} else {
// アンケート
Expand All @@ -967,6 +986,7 @@ public function publicConfirm($request, $page_id, $frame_id, $id = null)
'form' => $form,
'forms_columns' => $forms_columns,
'uploads' => $uploads,
'has_honeypot' => $has_honeypot,
]);
}
}
Expand Down Expand Up @@ -1022,6 +1042,18 @@ public function publicStore($request, $page_id, $frame_id, $id = null)
return collect(['redirect_path' => url($this->page->permanent_link)]);
}

// ハニーポットチェック(二重防御)
$honeypot_check = $this->checkHoneypot($request, $form);
if ($honeypot_check['blocked']) {
$this->recordHoneypotBlock($honeypot_check, $form->id);

// エラーメッセージをセッションに保存
session()->flash("spam_blocked_error_{$frame_id}", __('messages.honeypot_blocked'));

// 初期表示にリダイレクトして、初期表示処理にまかせる(エラー表示)
return collect(['redirect_path' => url($this->page->permanent_link)]);
}

// スパムフィルタリングチェック(二重防御)
$spam_check = $this->checkSpamFilter($request, $form);
if ($spam_check['blocked']) {
Expand Down Expand Up @@ -3210,6 +3242,101 @@ private function recordSpamBlock(array $spam_check, int $forms_id): void
}
}

/**
* ハニーポットチェック
*
* @param \Illuminate\Http\Request $request リクエスト
* @param Forms $form フォームデータ
* @return array ブロック情報の配列
*/
private function checkHoneypot($request, $form): array
{
$client_ip = $request->ip();

// このフォーム用のハニーポット設定を取得
$honeypot_spam_list = SpamList::where('target_plugin_name', 'forms')
->where('block_type', SpamBlockType::honeypot)
->where(function ($q) use ($form) {
$q->where('target_id', $form->id)
->orWhereNull('target_id');
})
->first();

// ハニーポットが設定されていなければ早期リターン
if (!$honeypot_spam_list) {
return [
'blocked' => false,
'client_ip' => $client_ip,
'matched_spam_list' => null,
];
}

// ハニーポットフィールドに値があればボットと判定
$honeypot_value = $request->input('website_url', '');
if (!empty($honeypot_value)) {
return [
'blocked' => true,
'client_ip' => $client_ip,
'honeypot_value' => $honeypot_value,
'matched_spam_list' => $honeypot_spam_list,
];
}

return [
'blocked' => false,
'client_ip' => $client_ip,
'matched_spam_list' => $honeypot_spam_list,
];
}

/**
* ハニーポットブロックのログ記録とDB履歴記録
*
* @param array $honeypot_check checkHoneypot() の戻り値
* @param int $forms_id フォームID
* @return void
*/
private function recordHoneypotBlock(array $honeypot_check, int $forms_id): void
{
Log::info('Honeypot blocked', [
'form_id' => $forms_id,
'ip' => $honeypot_check['client_ip'],
'honeypot_value' => $honeypot_check['honeypot_value'] ?? null,
'spam_list_id' => $honeypot_check['matched_spam_list']->id ?? null,
]);

// 履歴記録は補助的な機能のため、DB記録が失敗しても本来のブロック処理(エラーメッセージ表示)を続行する
try {
SpamBlockHistory::create([
'spam_list_id' => $honeypot_check['matched_spam_list']->id ?? null,
'forms_id' => $forms_id,
'block_type' => SpamBlockType::honeypot,
'block_value' => $honeypot_check['honeypot_value'] ?? null,
'client_ip' => $honeypot_check['client_ip'],
'submitted_email' => null,
]);
} catch (\Exception $e) {
Log::error('Failed to record honeypot block history', ['error' => $e->getMessage()]);
}
}

/**
* ハニーポットが設定されているか確認
*
* @param Forms $form フォームデータ
* @return bool ハニーポットが設定されている場合true
*/
private function hasHoneypot($form): bool
{
return SpamList::where('target_plugin_name', 'forms')
->where('block_type', SpamBlockType::honeypot)
->where(function ($q) use ($form) {
$q->where('target_id', $form->id)
->orWhereNull('target_id');
})
->exists();
}

/**
* スパムフィルタリングチェック
*
Expand Down Expand Up @@ -3366,10 +3493,15 @@ public function saveSpamFilter($request, $page_id, $frame_id, $forms_id)
*/
public function addSpamList($request, $page_id, $frame_id, $forms_id)
{
// ハニーポットの場合は値不要、それ以外は必須
$block_value_rules = $request->block_type === SpamBlockType::honeypot
? ['nullable', 'max:255']
: ['required', 'max:255'];

// 項目のエラーチェック
$validator = Validator::make($request->all(), [
'block_type' => ['required', 'in:' . implode(',', SpamBlockType::getMemberKeys())],
'block_value' => ['required', 'max:255'],
'block_value' => $block_value_rules,
]);
$validator->setAttributeNames([
'block_type' => '種別',
Expand All @@ -3383,12 +3515,17 @@ public function addSpamList($request, $page_id, $frame_id, $forms_id)
->withInput();
}

// ハニーポットの場合は値をnullにする
$block_value = $request->block_type === SpamBlockType::honeypot
? null
: $request->block_value;

// スパムリストの追加
SpamList::create([
'target_plugin_name' => 'forms',
'target_id' => $forms_id,
'block_type' => $request->block_type,
'block_value' => $request->block_value,
'block_value' => $block_value,
'memo' => $request->memo,
]);

Expand Down
1 change: 1 addition & 0 deletions config/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@
'MenuFrameConfig' => \App\Enums\MenuFrameConfig::class,
'MailAuthMethod' => \App\Enums\MailAuthMethod::class,
'PhotoalbumPlayviewType' => \App\Enums\PhotoalbumPlayviewType::class,
'SpamBlockType' => \App\Enums\SpamBlockType::class,

// utils
'DateUtils' => \App\Utilities\Date\DateUtils::class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class MakeBlockValueNullableInSpamLists extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('spam_lists', function (Blueprint $table) {
// ハニーポット対応: block_value をnullable に変更
// ハニーポットは値を必要としないため
$table->string('block_value', 255)->nullable()->comment('ブロック対象の値(ハニーポットの場合はnull)')->change();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('spam_lists', function (Blueprint $table) {
$table->string('block_value', 255)->nullable(false)->comment('ブロック対象の値')->change();
});
}
}
12 changes: 12 additions & 0 deletions public/css/connect.css
Original file line number Diff line number Diff line change
Expand Up @@ -569,3 +569,15 @@ a.cc-cursor-text:hover {
0% {opacity: 0;}
100% {opacity: 1;}
}

/* ハニーポット(スパムボット対策用の隠しフィールド)
------------------------------------- */
.connect-hp-field {
opacity: 0 !important;
position: absolute !important;
top: 0;
left: 0;
height: 0;
width: 0;
z-index: -1;
}
1 change: 1 addition & 0 deletions resources/lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
'cannot_be_delete_refers_to_the_information' => 'It cannot be deleted because there is a part that refers to the information you are trying to delete.',
'there_is_an_error' => 'There is an error.',
'there_is_an_error_refer_to_the_message_of_each_item' => 'For details of the error, refer to the message of each item.',
'honeypot_blocked' => 'Invalid submission detected.',
'both_required' => 'Please enter both.',
'search_results' => 'Search Results',
'cases' => 'cases',
Expand Down
1 change: 1 addition & 0 deletions resources/lang/ja/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
'cannot_be_delete_refers_to_the_information' => '削除しようとしている情報を参照している箇所がある為、削除できません。',
'there_is_an_error' => 'エラーがあります。',
'there_is_an_error_refer_to_the_message_of_each_item' => 'エラーの詳細は各項目のメッセージを参照してください。',
'honeypot_blocked' => '不正な投稿が検出されました。',
'both_required' => '両方の項目を入力してください。',
'search_results' => '検索結果',
'cases' => '件',
Expand Down
Loading