laravel-security

laravel-security

熱門

Laravel 安全最佳實踐——涵蓋身份驗證、授權機制、Eloquent 查詢安全、CSRF 與 XSS 防禦、API 安全以及安全的正式環境部署配置。

23萬星標
3.5萬分支
更新於 2026/7/14
SKILL.md
唯讀
名稱
laravel-security
描述

Laravel 安全最佳實踐——涵蓋身份驗證、授權機制、Eloquent 查詢安全、CSRF 與 XSS 防禦、API 安全以及安全的正式環境部署配置。

Laravel 安全最佳實踐

針對 Laravel 應用程式的完整安全指引,有效防禦常見的漏洞與安全威脅。

觸發時機

  • 設定 Laravel 身份驗證與授權機制(Sanctum、Passport、Jetstream、Breeze)
  • 實作使用者角色、權限控管與 Policy 策略
  • 配置正式環境(Production)安全設定與環境變數
  • 審查 Laravel 應用程式的安全漏洞
  • 將 Laravel 應用程式部署至正式環境
  • 撰寫安全的 Eloquent 查詢與 Migration 資料庫遷移

正式環境配置

關鍵正式環境設定

// 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 或 Service Provider
if (empty(config('app.key'))) {
    throw new RuntimeException('APP_KEY is not set. Run: 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("Missing required config key: {$key}");
    }
}

強制使用 HTTPS

// AppServiceProvider::boot() 或 Middleware
if (app()->environment('production')) {
    URL::forceScheme('https');
    request()->server->set('HTTPS', 'on');
}

// config/app.php 設定信任的代理伺服器(如 Load Balancers)
// 請使用具體的 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'],

// 透過 Middleware 在正式環境中強制使用 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 Token 身份驗證)

// 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, // Token 過期時間(單位:分鐘,null 代表永不過期)
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),

// 發行帶有權限(Abilities)的 Token
$token = $user->createToken('api-token', ['read', 'write'])->plainTextToken;

// 在路由上驗證 Token 權限
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(), // 檢查密碼是否已在洩漏資料庫(Have I Been Pwned)中曝光
        ],
    ];
}

// 限制登入嘗試次數
// App\Http\Controllers\Auth\AuthenticatedSessionController
protected function authenticated(Request $request, $user)
{
    if ($user->wasRecentlyLockedOut()) {
        // 發送異常登入通知給使用者
        $user->notify(new SuspiciousLoginNotification($request->ip()));
    }
}

Session 管理

// 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),

// 登入時重新產生 Session
// App\Http\Controllers\Auth\AuthenticatedSessionController
public function store(LoginRequest $request): RedirectResponse
{
    $request->authenticate();
    $request->session()->regenerate(); // 關鍵:防止 Session 固定攻擊(Session Fixation)
    return redirect()->intended(RouteServiceProvider::HOME);
}

// 登出時註銷 Session
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() 讓最高管理員(Super-admin)覆寫所有權限
    Gate::before(function (User $user, string $ability): ?bool {
        if ($user->role === 'super-admin') {
            return true; // 授予所有權限
        }
        return null; // 回歸一般權限檢查
    });
}

// 在 Controller 中的用法
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(); // 必須先完成 Email 驗證
    }

    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,
];

// 在 Controller 中的用法
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) }}">Edit</a>
@endcan

@cannot('update', $post)
    <span>You cannot edit this post</span>
@endcannot

中間件(Middleware)授權

// 在路由中使用 Middleware
Route::put('/posts/{post}', [PostController::class, 'update'])
    ->middleware('can:update,post');

Route::get('/posts/create', [PostController::class, 'create'])
    ->middleware('can:create,App\Models\Post');

// 自訂授權 Middleware
// 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, 'Unauthorized. This area requires role: ' . $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 安全

批量賦值(Mass Assignment)防護

// 錯誤:$guarded = [] 會允許所有欄位被批量賦值
// 切勿在正式環境中使用 $guarded = []

// 正確:將可填寫的屬性列入白名單($fillable)
final class User extends Authenticatable
{
    protected $fillable = [
        'name',
        'email',
        'phone',
        'avatar',
    ];
    // 切勿在此加入 'role'、'is_admin'、'is_verified'
}

// 正確:明確控管 Request 中哪些欄位可以填寫
public function store(StoreUserRequest $request): RedirectResponse
{
    $user = User::create($request->safe()->only([
        'name', 'email', 'phone', 'avatar'
    ]));
    // $request->safe() 僅會使用通過驗證的資料
    // 若未搭配驗證規則,單獨使用 $request->only() 是不安全的
}

// 錯誤:直接使用 Request 資料建立使用者
User::create($request->all()); // 存在批量賦值漏洞!

// 更好:使用通過驗證的資料來建立
$user = User::create($request->validated()); // 僅包含已驗證欄位

SQL 注入(SQL Injection)防禦

// 正確:Eloquent 會自動對查詢進行參數化處理
User::where('email', $userInput)->first();
User::whereRaw('email = ?', [$userInput])->first();

// 正確:Query Builder 也會進行參數化
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}')"); // 存在漏洞!

屬性型態轉換(Attribute Casting)

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+ 設定時自動加密雜湊
    ];
}

Model 模型安全

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
{
    // 全域 Scope 用於過濾軟刪除紀錄
    use SoftDeletes;

    // 透過限制延遲載入(Lazy Loading)來防止 N+1 問題(可選的嚴格模式)
    // AppServiceProvider::boot()
    // Model::preventLazyLoading(!app()->isProduction());
}

CSRF 防護

預設防護

// Laravel 預設已透過 VerifyCsrfToken Middleware 啟用 CSRF 防護
// app/Http/Kernel.php (protected $middlewareGroups['web'])

// 所有 POST/PUT/PATCH/DELETE 表單都必須包含 @csrf
<form method="POST" action

<!-- truncated for translation batch; full body continues in source -->