Communitygithub.com

laravel-security

Laravel 安全最佳實踐 — 認證、授權、Eloquent 安全性、CSRF、XSS 防護、API 安全及安全部署配置。在審查 Laravel 認證、Eloquent 安全性、CSRF、XSS、API 安全或部署配置時使用。

laravel-security 是什麼?

這個技能提供了全面的 Laravel 應用程式安全指南,涵蓋了認證、授權、Eloquent ORM 安全性、CSRF 和 XSS 防護、API 安全以及安全部署配置等關鍵領域。它支援標準的 Laravel 認證套件(Sanctum、Passport、Jetstream、Breeze),並提供詳細的程式碼範例,展示如何設定強固的授權機制(Gate 和 Policy)、防止大量賦值漏洞、避免 SQL 注入,以及正確處理環境變數和 HTTPS 強制。該技能還強調了 Blades 模板引擎中的安全輸出、使用 HTMLPurifier 過濾不受信任的 HTML,以及配置 Sanctum 令牌過期和作用域。透過遵循這些經實務驗證的步驟,開發者可以保護 Laravel 應用程式免受常見的 Web 漏洞侵害,並確保生產環境的安全性。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/laravel-security

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

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 安全性標頭
  • 日誌僅記錄必要的資訊
  • 生產環境中已停用偵錯模式

資源

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

相關技能