Laravel 安全最佳實踐
全面的 Laravel 應用程式安全指南,用於防範常見漏洞。
何時啟用
- 設定 Laravel 認證與授權(Sanctum、Passport、Jetstream、Breeze)
- 實作使用者角色、權限與政策
- 配置生產環境安全設定與環境變數
- 審查 Laravel 應用程式的安全性漏洞
- 將 Laravel 應用程式部署到生產環境
- 撰寫安全的 Eloquent 查詢與遷移
生產環境配置
重要的生產環境設定
// config/app.php
'env' => env('APP_ENV', 'production'),
'debug' => (bool) env('APP_DEBUG', false), // 重要:生產環境絕不能設為 true
'key' => env('APP_KEY'), // 必須設定:php artisan key:generate
// config/session.php
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => 'lax',
// 在啟動時驗證 APP_KEY
// bootstrap/app.php 或服務提供者
if (empty(config('app.key'))) {
throw new RuntimeException('未設定 APP_KEY。請執行:php artisan key:generate');
}
環境檔案安全
# 絕不要將 .env 提交到版本控制
# .gitignore 預設已包含 .env
# 改用 .env.example 並使用佔位符
DB_PASSWORD=
APP_KEY=
SANCTUM_TOKEN_PREFIX=
# 在啟動時驗證必要變數
// 在 AppServiceProvider::boot() 中
$requiredKeys = ['app.key', 'database.connections.mysql.database', 'database.connections.mysql.username'];
foreach ($requiredKeys as $key) {
if (empty(config($key))) {
throw new RuntimeException("缺少必要的配置鍵:{$key}");
}
}
HTTPS 強制
// AppServiceProvider::boot() 或中介層
if (app()->environment('production')) {
URL::forceScheme('https');
request()->server->set('HTTPS', 'on');
}
// config/app.php 設定受信任的代理(負載平衡器)
// 使用特定 IP 範圍 – * 會信任所有代理,允許 X-Forwarded-* 偽造
// AWS: '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'
'trusted_proxies' => ['10.0.0.0/8', '172.16.0.0/12'],
// 透過中介層在生產環境強制 HTTPS
// app/Http/Middleware/ForceHttps.php
public function handle($request, Closure $next)
{
if (!$request->secure() && app()->environment('production')) {
return redirect()->secure($request->getRequestUri());
}
return $next($request);
}
認證
Sanctum(API 令牌認證)
// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ',' . parse_url(env('APP_URL'), PHP_URL_HOST) : ''
)));
'expiration' => 60 * 24, // 令牌過期時間(分鐘),null 表示永不過期
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
// 使用能力範圍發出令牌
$token = $user->createToken('api-token', ['read', 'write'])->plainTextToken;
// 在路由上驗證能力
Route::middleware('auth:sanctum')->group(function () {
Route::get('/orders', function () {
// 使用者必須具有 'read' 能力
abort_unless(Auth::user()->tokenCan('read'), 403);
// ...
})->middleware('abilities:read');
Route::post('/orders', function () {
// 使用者必須具有 'write' 能力
abort_unless(Auth::user()->tokenCan('write'), 403);
// ...
})->middleware('abilities:write');
});
密碼安全
// config/hashing.php
// 預設為 bcrypt。Argon2id 更安全。
'bcrypt' => [
'rounds' => env('BCRYPT_ROUNDS', 12), // 增加以加強雜湊強度
],
'argon' => [
'memory' => 65536,
'threads' => 4,
'time' => 4,
],
// 在 RegisterRequest 中驗證密碼
public function rules(): array
{
return [
'password' => [
'required',
'confirmed',
Password::min(12)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised(), // 檢查 haveibeenpwned
],
];
}
// 限制登入嘗試次數
// App\Http\Controllers\Auth\AuthenticatedSessionController
protected function authenticated(Request $request, $user)
{
if ($user->wasRecentlyLockedOut()) {
// 通知使用者可疑登入
$user->notify(new SuspiciousLoginNotification($request->ip()));
}
}
工作階段管理
// config/session.php
'driver' => env('SESSION_DRIVER', 'database'), // database/redis > file
'lifetime' => env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
'encrypt' => env('SESSION_ENCRYPT', false),
// 登入時重新產生工作階段
// App\Http\Controllers\Auth\AuthenticatedSessionController
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate(); // 重要:防止工作階段固定攻擊
return redirect()->intended(RouteServiceProvider::HOME);
}
// 登出時失效工作階段
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
授權
Gate
// App\Providers\AuthServiceProvider
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::define('update-post', function (User $user, Post $post): bool {
return $user->id === $post->user_id;
});
Gate::define('publish-post', function (User $user): bool {
return $user->role === 'editor' || $user->role === 'admin';
});
// 使用 before() 進行超級管理員覆蓋
Gate::before(function (User $user, string $ability): ?bool {
if ($user->role === 'super-admin') {
return true; // 授予所有能力
}
return null; // 回退到一般檢查
});
}
// 在控制器中使用
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update-post', $post);
// 或:$this->authorize('update-post', $post);
// 或:abort_unless(Auth::user()->can('update-post', $post), 403);
// ...
}
Policy
// App\Policies\PostPolicy
class PostPolicy
{
use HandlesAuthorization;
public function viewAny(?User $user): bool
{
return true; // 公開列表
}
public function view(?User $user, Post $post): bool
{
return $post->is_published || ($user && $user->id === $post->user_id);
}
public function create(User $user): bool
{
return $user->hasVerifiedEmail(); // 必須先驗證電子郵件
}
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id && $post->created_at->diffInDays(now()) <= 30;
}
public function restore(User $user, Post $post): bool
{
return $user->role === 'admin';
}
public function forceDelete(User $user, Post $post): bool
{
return $user->role === 'super-admin';
}
}
// 在 AuthServiceProvider 中註冊
protected $policies = [
Post::class => PostPolicy::class,
];
// 控制器使用
public function show(Post $post): View
{
$this->authorize('view', $post);
return view('posts.show', compact('post'));
}
// Blade 使用
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">編輯</a>
@endcan
@cannot('update', $post)
<span>您無法編輯此文章</span>
@endcannot
中介層授權
// 在路由中使用中介層
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update,post');
Route::get('/posts/create', [PostController::class, 'create'])
->middleware('can:create,App\Models\Post');
// 自訂授權中介層
// app/Http/Middleware/CheckRole.php
class CheckRole
{
public function handle(Request $request, Closure $next, string $role): mixed
{
if (!$request->user() || $request->user()->role !== $role) {
abort(403, '未經授權。此區域需要角色:' . $role);
}
return $next($request);
}
}
// 在 Kernel 中註冊
protected $routeMiddleware = [
'role' => \App\Http\Middleware\CheckRole::class,
];
// 路由使用
Route::middleware(['auth', 'role:admin'])->group(function () {
Route::get('/admin', [AdminController::class, 'index']);
});
Eloquent 安全性
大量賦值保護
// 不良做法:$guarded = [] 允許所有欄位被大量賦值
// 絕不要在生產環境中使用 $guarded = []
// 良好做法:白名單 fillable 屬性
final class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'phone',
'avatar',
];
// 絕不要在此加入 'role'、'is_admin'、'is_verified'
}
// 良好做法:明確控制在請求中可以填入的欄位
public function store(StoreUserRequest $request): RedirectResponse
{
$user = User::create($request->safe()->only([
'name', 'email', 'phone', 'avatar'
]));
// $request->safe() 僅使用經過驗證的資料
// $request->only() 若無驗證規則,本身並不安全
}
// 不良做法:直接使用請求資料建立使用者
User::create($request->all()); // 容易受到大量賦值攻擊!
// 更好做法:使用 DTO 進行建立
$user = User::create($request->validated()); // 僅使用經驗證的欄位
SQL 注入防護
// 良好做法:Eloquent 自動參數化查詢
User::where('email', $userInput)->first();
User::whereRaw('email = ?', [$userInput])->first();
// 良好做法:查詢建構器也自動參數化
DB::table('users')->where('email', $userInput)->first();
DB::select('SELECT * FROM users WHERE email = ?', [$userInput]);
// 不良做法:原始字串內插
DB::select("SELECT * FROM users WHERE email = '{$userInput}'"); // 容易受到攻擊!
User::whereRaw("email = '{$userInput}'")->first(); // 容易受到攻擊!
// 不良做法:使用未跳脫輸入的 whereRaw/orderByRaw
User::orderByRaw($userInput); // 容易受到攻擊!
User::groupByRaw($userInput); // 容易受到攻擊!
// 不良做法:使用字串連接的 DB::statement
DB::statement("INSERT INTO users (email) VALUES ('{$userInput}')"); // 容易受到攻擊!
屬性型別轉換
final class User extends Authenticatable
{
protected $casts = [
'email_verified_at' => 'datetime',
'is_admin' => 'boolean', // 轉換為布林值可防止字串注入
'settings' => 'array', // 自動 json_encode/json_decode
'metadata' => 'encrypted:array', // Laravel 11+ 加密轉換
'password' => 'hashed', // Laravel 10+ 設定時自動雜湊
];
}
模型安全性
final class User extends Authenticatable
{
// 從 JSON/API 回應中隱藏敏感屬性
protected $hidden = [
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
];
// 僅附加安全的計算屬性
protected $appends = ['full_name']; // 安全
// 絕不要附加敏感的計算資料
}
final class Post extends Model
{
// 全域範圍來過濾軟刪除記錄
use SoftDeletes;
// 透過限制惰性載入來防止 N+1(可選的嚴格模式)
// AppServiceProvider::boot()
// Model::preventLazyLoading(!app()->isProduction());
}
CSRF 防護
預設防護
// Laravel CSRF 預設透過 VerifyCsrfToken 中介層啟用
// app/Http/Kernel.php(protected $middlewareGroups['web'])
// 所有 POST/PUT/PATCH/DELETE 表單必須包含 @csrf
<form method="POST" action="/posts">
@csrf
<input type="text" name="title">
<button type="submit">建立</button>
</form>
排除路由(請謹慎)
// app/Http/Middleware/VerifyCsrfToken.php
class VerifyCsrfToken extends Middleware
{
// 僅排除具有外部 CSRF 防護的路由(Webhook 等)
protected $except = [
'stripe/*', // Stripe Webhook 使用自己的簽章驗證
// 避免使用通用 'api/*' —— 有狀態的 Sanctum 路由需要 CSRF。
// 僅排除特定的無狀態 Webhook/端點路由。
];
}
搭配 JavaScript 使用 CSRF
<meta name="csrf-token" content="{{ csrf_token() }}">
<script>
// Axios 範例(Laravel 內建 Axios)
axios.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector(
'meta[name="csrf-token"]'
).getAttribute('content');
// Fetch 範例
fetch('/posts', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
</script>
XSS 防護
Blade 模板安全
{{-- 安全:Blade 自動跳脫 --}}
{{ $userInput }}
{{-- 危險:原始輸出 — 絕不要與使用者輸入一起使用 --}}
{!! $userInput !!}
{{-- 安全:僅將 {!! !!} 用於您控制的受信任內容 --}}
{!! $trustedHtmlFromYourServer !!}
{{-- 良好做法:使用特定的跳脫指令 --}}
@js($data) {{-- 為 JavaScript 進行 JSON 編碼 --}}
@json($data) {{-- 在模板中進行 JSON 編碼 --}}
{{-- 不良做法:直接在原始 HTML 中使用使用者輸入 --}}
<div>{!! $user->bio !!}</div> {{-- 如果使用者提供 bio,則容易受到攻擊 --}}
安全 HTML 處理
// 當必須允許某些 HTML 時,使用白名單方法
use HTMLPurifier; // 需要:composer require ezyang/htmlpurifier
public function sanitizeHtml(string $dirty): string
{
$config = \HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', 'p,b,i,a[href],ul,ol,li,br');
$config->set('URI.AllowedSchemes', ['http', 'https', 'mailto']);
$purifier = new \HTMLPurifier($config);
return $purifier->purify($dirty);
}
// 在 Blade 中:
<div>{!! $sanitizedContent !!}</div>
API 安全性
Sanctum API 認證
// routes/api.php 設定
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
// 為 SPA 設定有狀態 API
// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'yourdomain.com,localhost:3000')),
// 在 Kernel.php 中的 API 中介層群組
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
// 令牌撤銷
// 登出時撤銷所有令牌
$user->tokens()->delete();
// 撤銷特定令牌
$user->tokens()->where('id', $tokenId)->delete();
Laravel Passport(OAuth2 伺服器)
// 密碼授權類型安全性
// AppServiceProvider::boot()
Passport::enablePasswordGrant();
// 設定令牌過期時間
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::personalAccessTokensExpireIn(now()->addMonths(1));
// 撤銷令牌
$token = $request->user()->token();
$token->revoke();
// 僅允許白名單的用戶端
Passport::client()->where('revoked', false)->get();
API 速率限制
// app/Http/Kernel.php
'api' => [
'throttle:60,1', // 每分鐘 60 次請求
];
// 按使用者或 IP 進行細粒度限制
Route::middleware('throttle:rate_limit,1')->group(function () {
Route::get('/user', function () {
// ...
});
});
// 在控制器中動態設定限制
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
}
// 處理速率限制回應
// app/Exceptions/Handler.php
public function register(): void
{
$this->renderable(function (ThrottleRequestsException $e, $request) {
return response()->json([
'message' => '請求次數過多。請稍後再試。',
'retry_after' => $e->getHeaders()['Retry-After'] ?? null,
], 429);
});
}
請求驗證
// app/Http/Requests/StorePostRequest.php
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
// 已在 Controller/Policy 中處理授權
// 此處返回 true 並在控制器中授權
return true;
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'body' => 'required|string',
'category_id' => 'required|exists:categories,id', // 防止 IDOR
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
];
}
// 必要時覆蓋 failedValidation
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(response()->json([
'message' => '驗證失敗',
'errors' => $validator->errors(),
], 422));
}
}
資料庫安全性
安全遷移
// 設定預設字串長度(為了與舊版 MySQL 相容)
// AppServiceProvider::boot()
Schema::defaultStringLength(191);
// 安全索引
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
// 外鍵約束與索引
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
// 自動建立索引
$table->string('title');
$table->text('body');
$table->boolean('is_published')->default(false);
$table->timestamps();
});
SQL 注入防護(進階)
// 使用參數化查詢
User::where('email', $input)->first(); // 安全
// 在 raw 查詢中使用具名綁定
DB::select('SELECT * FROM users WHERE email = :email', ['email' => $input]); // 安全
// 使用白名單進行安全排序
$allowedSortColumns = ['name', 'email', 'created_at'];
$sortColumn = in_array($request->sort, $allowedSortColumns) ? $request->sort : 'created_at';
$sortDirection = $request->direction === 'desc' ? 'desc' : 'asc';
User::orderBy($sortColumn, $sortDirection)->get(); // 安全
// 使用模型 preventLazyLoading 進行 N+1 查詢防護
// AppServiceProvider::boot()
Model::preventLazyLoading(!app()->isProduction());
部署安全
環境特定配置
// .env.production
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_database
DB_USERNAME=your_username
DB_PASSWORD=your_strong_password
SESSION_DRIVER=redis
SESSION_SECURE_COOKIE=true
SANCTUM_STATEFUL_DOMAINS=yourdomain.com
伺服器強化
# 1. 設定正確的檔案權限
find . -type f -exec chmod 644 {} \;
find . -type d -exec chmod 755 {} \;
chmod -R 775 storage bootstrap/cache
chmod -R 775 public
# 2. 設定正確的擁有者
chown -R www-data:www-data /var/www/your-app
chown -R $USER:www-data storage bootstrap/cache
# 3. 移除未使用的檔案
rm -rf .git .github .gitignore
rm -rf tests/
rm -rf node_modules/
rm -rf resources/sass/
rm -f phpunit.xml .styleci.yml
# 4. 更新 Composer 依賴
composer install --no-dev --optimize-autoloader
# 5. 最佳化 Laravel
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
Web 伺服器配置
# /etc/nginx/sites-available/your-app
server {
listen 443 ssl http2;
server_name yourdomain.com;
root /var/www/your-app/public;
index index.php;
ssl_certificate /etc/ssl/certs/yourdomain.com.crt;
ssl_certificate_key /etc/ssl/private/yourdomain.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
# 安全性標頭
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
# 保護敏感檔案
location ~ \.env$ {
deny all;
}
location ~ \.git/ {
deny all;
}
}
日誌與監控
安全日誌
// config/logging.php
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single', 'slack'],
'level' => env('LOG_LEVEL', 'error'),
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'error'),
'permission' => 0640, // 限制日誌檔案權限
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Log',
'emoji' => ':boom:',
'level' => env('LOG_LEVEL', 'critical'),
],
];
監控安全事件
// App\Http\Middleware\LogSecurityEvents.php
class LogSecurityEvents
{
public function handle(Request $request, Closure $next): mixed
{
$response = $next($request);
// 記錄所有 403 和 500 錯誤
if (in_array($response->status(), [403, 500])) {
Log::warning('安全事件偵測', [
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'url' => $request->fullUrl(),
'user_id' => auth()->id(),
'status' => $response->status(),
]);
}
return $response;
}
}
// 監聽認證事件
// App\Providers\EventServiceProvider
protected $listen = [
\Illuminate\Auth\Events\Login::class => [
\App\Listeners\LogSuccessfulLogin::class,
],
\Illuminate\Auth\Events\Failed::class => [
\App\Listeners\LogFailedLogin::class,
],
\Illuminate\Auth\Events\OtherDeviceLogout::class => [
\App\Listeners\LogOtherDeviceLogout::class,
],
];
測試安全性
測試認證
test('未經認證的使用者無法存取儀表板', function () {
$response = $this->get('/dashboard');
$response->assertRedirect('/login');
});
test('一般使用者無法刪除其他使用者的文章', function () {
$user = User::factory()->create();
$otherUser = User::factory()->create();
$post = Post::factory()->create(['user_id' => $otherUser->id]);
$this->actingAs($user)
->delete("/posts/{$post->id}")
->assertForbidden();
});
test('僅管理員可以刪除文章', function () {
$admin = User::factory()->create(['role' => 'admin']);
$post = Post::factory()->create();
$this->actingAs($admin)
->delete("/posts/{$post->id}")
->assertSuccessful();
});
測試 XSS 防護
test('使用者輸入在輸出時被跳脫', function () {
$user = User::factory()->create([
'name' => '<script>alert("XSS")</script>',
]);
$response = $this->get("/users/{$user->id}");
$response->assertSee('<script>', false); // 確認 HTML 未直接輸出
$response->assertDontSee('<script>'); // 確認已安全跳脫
});
test('XSS 請求被拒絕', function () {
$response = $this->post('/comments', [
'body' => '<script>alert("XSS")</script>',
]);
$response->assertSessionHasErrors('body');
});
測試 CSRF 防護
test('缺少 CSRF 令牌的請求被拒絕', function () {
$response = $this->post('/posts', [
'title' => 'Test Post',
'body' => 'Test content',
]);
$response->assertStatus(419); // CSRF 令牌不匹配
});
測試 SQL 注入防護
test('SQL 注入嘗試被阻止', function () {
// 模擬 SQL 注入嘗試
$maliciousInput = "'; DROP TABLE users; --";
// Eloquent 查詢應安全處理
$user = User::where('email', $maliciousInput)->first();
expect($user)->toBeNull(); // 不應擲回例外
});
常見弱點與緩解措施
| 弱點 | 風險 | 緩解措施 |
|---|---|---|
| SQL 注入 | 資料庫遭到破壞 | 使用 Eloquent/參數化查詢 |
| XSS | 用戶端指令碼攻擊 | 跳脫輸出、CSP |
| CSRF | 跨站請求偽造 | @csrf 令牌、SameSite Cookie |
| 大量賦值 | 屬性操縱 | fillable、驗證、DTO |
| IDOR | 未經授權的資料存取 | 授權檢查、Policy |
| 密碼雜湊 | 身分盜竊 | bcrypt (12 輪)、Argon2id |
| 工作階段固定 | 工作階段劫持 | 登入時重新產生工作階段 |
| 缺乏速率限制 | 暴力破解 | 節流、鎖定 |
| 敏感資料暴露 | 資料外洩 | hidden、加密、環境變數 |
| 不安全的反序列化 | 遠端程式碼執行 | 安全的唯讀 DTO、驗證 |
快速檢查清單
- 生產環境中
APP_DEBUG設為false -
APP_KEY已設定且唯一 -
.env檔案未提交到版本控制 - HTTPS 已在生產環境中強制執行
- 密碼雜湊使用至少 12 輪的 bcrypt
- 所有模型都有正確的
$fillable或$guarded設定 - 密碼和令牌等敏感欄位已隱藏
- Eloquent 查詢使用參數化綁定
- 所有表單包含
@csrf - 所有使用者輸入在輸出時已跳脫
- 授權使用 Gate 和 Policy 實作
- 工作階段在登入時重新產生
- API 路由受速率限制保護
- Sanctum/Passport 令牌未公開暴露
- 部署了 Nginx 安全性標頭
- 日誌僅記錄必要的資訊
- 生產環境中已停用偵錯模式