@cristiancorreau/forge 3.1.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +265 -109
  3. package/assets/adapters/claude-code/commands/laravel-eloquent.md +7 -0
  4. package/assets/adapters/claude-code/commands/laravel-mcp.md +7 -0
  5. package/assets/adapters/claude-code/commands/laravel-pest.md +7 -0
  6. package/assets/adapters/claude-code/commands/laravel-security.md +7 -0
  7. package/assets/adapters/claude-code/commands/laravel-verify.md +7 -0
  8. package/assets/core/hooks/pre-bash-check.js +46 -0
  9. package/assets/core/hooks/pre-edit-check.js +14 -0
  10. package/assets/core/skills/laravel-eloquent/SKILL.md +453 -0
  11. package/assets/core/skills/laravel-mcp/SKILL.md +468 -0
  12. package/assets/core/skills/laravel-pest/SKILL.md +686 -0
  13. package/assets/core/skills/laravel-security/SKILL.md +658 -0
  14. package/assets/core/skills/laravel-verify/SKILL.md +462 -0
  15. package/assets/manifest.json +27 -2
  16. package/assets/profiles/astro/agents/frontend-engineer.md +2 -0
  17. package/assets/profiles/django/agents/api-engineer.md +2 -0
  18. package/assets/profiles/expo/agents/mobile-engineer.md +2 -0
  19. package/assets/profiles/express/agents/api-engineer.md +2 -0
  20. package/assets/profiles/fastapi/agents/api-engineer.md +2 -0
  21. package/assets/profiles/flask/agents/api-engineer.md +2 -0
  22. package/assets/profiles/flutter/agents/mobile-engineer.md +12 -10
  23. package/assets/profiles/go-gin/agents/api-engineer.md +3 -1
  24. package/assets/profiles/hono-drizzle/agents/api-engineer.md +2 -0
  25. package/assets/profiles/laravel/README.md +16 -2
  26. package/assets/profiles/laravel/agents/api-engineer.md +2 -0
  27. package/assets/profiles/laravel/agents/fullstack-engineer.md +4 -2
  28. package/assets/profiles/laravel/agents/laravel-specialist.md +607 -0
  29. package/assets/profiles/laravel/agents/laravel-test-engineer.md +448 -0
  30. package/assets/profiles/nestjs/agents/api-engineer.md +3 -1
  31. package/assets/profiles/nextjs-admin/agents/admin-engineer.md +2 -0
  32. package/assets/profiles/playwright-crawler/agents/scanner-engineer.md +2 -0
  33. package/assets/profiles/rails/agents/fullstack-engineer.md +2 -0
  34. package/assets/profiles/rust/agents/api-engineer.md +2 -0
  35. package/assets/profiles/springboot/agents/api-engineer.md +11 -9
  36. package/assets/profiles/sveltekit/agents/frontend-engineer.md +4 -2
  37. package/assets/profiles/vuenuxt/agents/frontend-engineer.md +12 -10
  38. package/assets/profiles/wordpress/agents/divi-engineer.md +2 -0
  39. package/assets/profiles/wordpress/agents/elementor-engineer.md +2 -0
  40. package/dist/cli.js +10 -0
  41. package/dist/cli.js.map +1 -1
  42. package/dist/commands/add.d.ts +2 -0
  43. package/dist/commands/add.d.ts.map +1 -0
  44. package/dist/commands/add.js +187 -0
  45. package/dist/commands/add.js.map +1 -0
  46. package/dist/commands/mcp.d.ts +42 -0
  47. package/dist/commands/mcp.d.ts.map +1 -0
  48. package/dist/commands/mcp.js +141 -0
  49. package/dist/commands/mcp.js.map +1 -0
  50. package/dist/lib/catalog.d.ts.map +1 -1
  51. package/dist/lib/catalog.js +5 -0
  52. package/dist/lib/catalog.js.map +1 -1
  53. package/dist/lib/mcp-tools.d.ts +37 -0
  54. package/dist/lib/mcp-tools.d.ts.map +1 -0
  55. package/dist/lib/mcp-tools.js +124 -0
  56. package/dist/lib/mcp-tools.js.map +1 -0
  57. package/dist/lib/skill-security.d.ts +66 -0
  58. package/dist/lib/skill-security.d.ts.map +1 -0
  59. package/dist/lib/skill-security.js +225 -0
  60. package/dist/lib/skill-security.js.map +1 -0
  61. package/dist/lib/skill-source.d.ts +29 -0
  62. package/dist/lib/skill-source.d.ts.map +1 -0
  63. package/dist/lib/skill-source.js +94 -0
  64. package/dist/lib/skill-source.js.map +1 -0
  65. package/dist/tui/dashboard.d.ts.map +1 -1
  66. package/dist/tui/dashboard.js +3 -6
  67. package/dist/tui/dashboard.js.map +1 -1
  68. package/dist/tui/panel.d.ts.map +1 -1
  69. package/dist/tui/panel.js +7 -18
  70. package/dist/tui/panel.js.map +1 -1
  71. package/dist/tui/wizard.d.ts.map +1 -1
  72. package/dist/tui/wizard.js +3 -13
  73. package/dist/tui/wizard.js.map +1 -1
  74. package/dist/ui/colors.d.ts +3 -1
  75. package/dist/ui/colors.d.ts.map +1 -1
  76. package/dist/ui/colors.js +11 -2
  77. package/dist/ui/colors.js.map +1 -1
  78. package/dist/ui/header.d.ts.map +1 -1
  79. package/dist/ui/header.js +4 -3
  80. package/dist/ui/header.js.map +1 -1
  81. package/dist/ui/theme.d.ts +24 -0
  82. package/dist/ui/theme.d.ts.map +1 -0
  83. package/dist/ui/theme.js +32 -0
  84. package/dist/ui/theme.js.map +1 -0
  85. package/dist/version.d.ts +1 -1
  86. package/dist/version.js +1 -1
  87. package/package.json +2 -2
@@ -0,0 +1,658 @@
1
+ # Skill: laravel-security
2
+
3
+ Seguridad de aplicaciones Laravel: auth, autorización, validación, mass assignment,
4
+ CSRF, rate limiting, inyección SQL, XSS, secrets, uploads y deploy seguro. Actívalo al
5
+ crear o revisar cualquier endpoint, modelo, formulario o configuración que toque datos
6
+ de usuario, autenticación o producción.
7
+
8
+ Triggers: /laravel-security, "seguridad laravel", "auth laravel", "policy laravel",
9
+ "form request", "mass assignment", "sanctum o passport", "csrf laravel", "rate limit laravel",
10
+ "inyección sql laravel", "blade xss", "asegurar endpoint laravel", "deploy seguro laravel".
11
+
12
+ ---
13
+
14
+ ## Cuándo usar este skill
15
+
16
+ - Al crear o modificar endpoints (rutas web o API) que requieren auth o autorización.
17
+ - Al definir o cambiar modelos Eloquent (`$fillable`/`$guarded`, casts).
18
+ - Al escribir validación de input: SIEMPRE en un Form Request, nunca inline en el controller.
19
+ - Al elegir el paquete de auth (Sanctum vs Fortify vs Passport).
20
+ - Al renderizar datos de usuario en Blade (`{{ }}` vs `{!! !!}`).
21
+ - Al manejar secrets, uploads, queries crudas o cualquier `DB::raw`.
22
+ - Antes de cada deploy a producción (`APP_DEBUG=false`, `key:generate`, HTTPS, headers).
23
+
24
+ > Las versiones recientes de Laravel usan **estructura slim** (PHP 8.3+): NO existe
25
+ > `app/Http/Kernel.php`. Todo el middleware se configura en `bootstrap/app.php` dentro de
26
+ > `->withMiddleware()`. Verifica la estructura leyendo `bootstrap/app.php` en tu proyecto.
27
+
28
+ ---
29
+
30
+ ## 1. Autenticación — elegir el paquete correcto
31
+
32
+ No uses Passport por defecto. La regla general:
33
+
34
+ | Paquete | Para qué | Cuándo |
35
+ |---------|----------|--------|
36
+ | **Sanctum** | Tokens de API + cookie de SPA + mobile, con abilities/scopes | Default para casi todo: SPA propia, app mobile, API first-party |
37
+ | **Fortify** | Backend headless de auth por **sesión** (sin UI) | App web first-party que renderiza su propio frontend |
38
+ | **Passport** | Servidor **OAuth2 / OAuth 2.1** completo | Solo clientes de terceros, "Log in with X", delegación, M2M (client-credentials) |
39
+
40
+ ```bash
41
+ # API / SPA / mobile (default):
42
+ composer require laravel/sanctum
43
+
44
+ # App web por sesión, sin frontend de auth:
45
+ composer require laravel/fortify
46
+
47
+ # Solo si necesitas un servidor OAuth2 real:
48
+ composer require laravel/passport
49
+ ```
50
+
51
+ **Sanctum — tokens de API con abilities (scopes):**
52
+
53
+ ```php
54
+ // Emitir un token con abilities acotadas (principio de mínimo privilegio).
55
+ $token = $user->createToken('mobile', ['posts:read', 'posts:create']);
56
+
57
+ return ['token' => $token->plainTextToken];
58
+ ```
59
+
60
+ ```php
61
+ // routes/api.php — proteger e inspeccionar abilities.
62
+ use Illuminate\Support\Facades\Route;
63
+
64
+ Route::middleware('auth:sanctum')->group(function () {
65
+ Route::post('/posts', [PostController::class, 'store'])
66
+ ->middleware('ability:posts:create');
67
+ });
68
+ ```
69
+
70
+ ```php
71
+ // Dentro del controller, verificación granular adicional.
72
+ if (! $request->user()->tokenCan('posts:create')) {
73
+ abort(403, 'Token sin permiso.');
74
+ }
75
+ ```
76
+
77
+ Reglas:
78
+
79
+ - El middleware de auth corre ANTES de cualquier lógica. Sin token/sesión válida → 401, sin procesar nada.
80
+ - Emite tokens con las abilities mínimas necesarias, no con `['*']`.
81
+ - Nunca confíes en headers que el cliente controla (`X-User-Id`, `X-Role`); deriva la identidad del token/sesión.
82
+
83
+ ---
84
+
85
+ ## 2. Autorización — Policies, Gates y `@can`
86
+
87
+ La autorización va en Policies (o Gates), nunca dispersa con `if` ad-hoc en cada controller.
88
+
89
+ ```bash
90
+ php artisan make:policy PostPolicy --model=Post
91
+ ```
92
+
93
+ ```php
94
+ // app/Policies/PostPolicy.php
95
+ namespace App\Policies;
96
+
97
+ use App\Models\Post;
98
+ use App\Models\User;
99
+
100
+ class PostPolicy
101
+ {
102
+ public function update(User $user, Post $post): bool
103
+ {
104
+ return $user->id === $post->user_id; // ownership check (anti-IDOR)
105
+ }
106
+
107
+ public function delete(User $user, Post $post): bool
108
+ {
109
+ return $user->id === $post->user_id || $user->is_admin;
110
+ }
111
+ }
112
+ ```
113
+
114
+ ```php
115
+ // En el controller: Gate::authorize() lanza 403 automáticamente si falla.
116
+ use Illuminate\Support\Facades\Gate;
117
+
118
+ public function update(UpdatePostRequest $request, Post $post)
119
+ {
120
+ Gate::authorize('update', $post); // resuelve la Policy y aborta con 403 si no pasa
121
+
122
+ $post->update($request->validated());
123
+
124
+ return $post->toResource(); // las versiones recientes auto-descubren PostResource
125
+ }
126
+ ```
127
+
128
+ > En el esqueleto slim de Laravel el controller base (`App\Http\Controllers\Controller`)
129
+ > suele estar vacío: puede NO incluir el trait `AuthorizesRequests`, así que `$this->authorize(...)`
130
+ > no existiría por defecto. Verifica leyendo el controller base de tu proyecto. Usa
131
+ > `Gate::authorize(...)` (funciona sin trait) o el middleware `can:`. Si prefieres
132
+ > `$this->authorize(...)`, agrega `use AuthorizesRequests;` al controller base.
133
+
134
+ ```php
135
+ // Atajos útiles para route model binding: autoriza vía la Policy antes del controller.
136
+ Route::put('/posts/{post}', [PostController::class, 'update'])
137
+ ->middleware('can:update,post'); // no requiere ningún trait en el controller
138
+ ```
139
+
140
+ ```blade
141
+ {{-- En Blade: nunca muestres acciones que el usuario no puede ejecutar. --}}
142
+ @can('update', $post)
143
+ <a href="{{ route('posts.edit', $post) }}">Editar</a>
144
+ @endcan
145
+
146
+ @cannot('delete', $post)
147
+ <span class="text-muted">No puedes borrar este post</span>
148
+ @endcannot
149
+ ```
150
+
151
+ **IDOR (Insecure Direct Object Reference):** al cargar un recurso por ID, verifica SIEMPRE
152
+ ownership/permiso en la Policy. No asumas que un ID "difícil de adivinar" protege nada. Que el
153
+ recurso aparezca en route model binding NO implica que el usuario actual pueda verlo.
154
+
155
+ ---
156
+
157
+ ## 3. Validación — siempre en un Form Request
158
+
159
+ Nunca valides en el cuerpo del controller con `$request->validate([...])` para nada no trivial.
160
+ Usa un Form Request: combina **autorización + validación** y mantiene el controller limpio.
161
+
162
+ ```bash
163
+ php artisan make:request StorePostRequest
164
+ ```
165
+
166
+ ```php
167
+ // app/Http/Requests/StorePostRequest.php
168
+ namespace App\Http\Requests;
169
+
170
+ use Illuminate\Foundation\Http\FormRequest;
171
+ use Illuminate\Validation\Rule;
172
+
173
+ class StorePostRequest extends FormRequest
174
+ {
175
+ public function authorize(): bool
176
+ {
177
+ // La autorización corre ANTES de validar. false => 403 automático.
178
+ return $this->user()->can('create', \App\Models\Post::class);
179
+ }
180
+
181
+ public function rules(): array
182
+ {
183
+ return [
184
+ 'title' => ['required', 'string', 'max:255'],
185
+ 'body' => ['required', 'string'],
186
+ 'status' => ['required', Rule::in(['draft', 'published'])],
187
+ 'tags' => ['array', 'max:10'],
188
+ 'tags.*' => ['string', 'max:30'],
189
+ ];
190
+ }
191
+
192
+ // Normaliza input ANTES de validar (no para confiar en él, sino para validarlo bien).
193
+ protected function prepareForValidation(): void
194
+ {
195
+ $this->merge(['title' => trim((string) $this->input('title'))]);
196
+ }
197
+ }
198
+ ```
199
+
200
+ ```php
201
+ // Controller: la validación corre automáticamente al type-hintear el Form Request.
202
+ public function store(StorePostRequest $request)
203
+ {
204
+ // Usa SOLO los datos validados; nunca $request->all() ni $request->input() crudo.
205
+ $post = $request->user()->posts()->create($request->validated());
206
+
207
+ // Alternativa con whitelist explícita:
208
+ // $post->fill($request->safe()->only(['title', 'body', 'status']));
209
+
210
+ return $post->toResource();
211
+ }
212
+ ```
213
+
214
+ Reglas:
215
+
216
+ - `authorize(): bool` controla el acceso; `rules()` controla la forma del input. Los dos siempre.
217
+ - Recupera datos con `$request->validated()` o `$request->safe()->only([...])` — nunca `->all()`.
218
+ - Prefiere la sintaxis de array de reglas (`['required', 'max:255']`) sobre el string `'required|max:255'`.
219
+ - Una regla `confirmed`, `exists`, `unique` mal puesta es un bug de seguridad (p. ej. `email` sin `unique`).
220
+
221
+ ---
222
+
223
+ ## 4. Mass assignment — `$fillable` o `$guarded`
224
+
225
+ Sin protección, un atacante envía `is_admin=1` o `user_id=99` en el body y Eloquent lo persiste.
226
+
227
+ ```php
228
+ // app/Models/Post.php
229
+ namespace App\Models;
230
+
231
+ use Illuminate\Database\Eloquent\Model;
232
+
233
+ class Post extends Model
234
+ {
235
+ // Whitelist explícita: SOLO estos campos son asignables en masa.
236
+ protected $fillable = ['title', 'body', 'status'];
237
+
238
+ // NUNCA pongas en $fillable: id, user_id, is_admin, role, email_verified_at, etc.
239
+ // Esos campos se asignan a mano desde el servidor con valores de confianza.
240
+
241
+ // En las versiones recientes de Laravel, define casts con el MÉTODO casts(),
242
+ // no la propiedad legacy $casts. Verifica el estilo soportado por tu versión.
243
+ protected function casts(): array
244
+ {
245
+ return [
246
+ 'is_admin' => 'boolean',
247
+ 'published_at' => 'datetime',
248
+ ];
249
+ }
250
+ }
251
+ ```
252
+
253
+ ```php
254
+ // Asigna los campos sensibles desde el servidor, fuera de la asignación masiva.
255
+ $post = $request->user()->posts()->create($request->validated()); // user_id viene de la relación
256
+ $post->forceFill(['approved_by' => auth()->id()])->save(); // valor de confianza
257
+ ```
258
+
259
+ Endurecimiento global recomendado (atrapa silencios peligrosos en dev/test):
260
+
261
+ ```php
262
+ // app/Providers/AppServiceProvider.php — boot()
263
+ use Illuminate\Database\Eloquent\Model;
264
+
265
+ public function boot(): void
266
+ {
267
+ // Lanza excepción si se intenta asignar un atributo fuera de $fillable.
268
+ Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
269
+ Model::preventAccessingMissingAttributes(! $this->app->isProduction());
270
+ }
271
+ ```
272
+
273
+ Regla: `$fillable` (allowlist) sobre `$guarded = []` (que abre todo). Nunca uses `$guarded = []`
274
+ combinado con `Model::create($request->all())`.
275
+
276
+ ---
277
+
278
+ ## 5. CSRF — `PreventRequestForgery`
279
+
280
+ En las versiones recientes de Laravel el middleware de CSRF se llama **`PreventRequestForgery`**
281
+ (renombrado desde `VerifyCsrfToken`) y ya está en el grupo `web` por defecto. Con la estructura
282
+ slim no hay `app/Http/Kernel.php`: si necesitas excluir rutas (p. ej. webhooks), se configura en
283
+ `bootstrap/app.php`. Verifica el nombre del middleware en la documentación oficial de tu versión.
284
+
285
+ ```php
286
+ // bootstrap/app.php
287
+ use Illuminate\Foundation\Application;
288
+ use Illuminate\Foundation\Configuration\Middleware;
289
+
290
+ return Application::configure(basePath: dirname(__DIR__))
291
+ ->withMiddleware(function (Middleware $middleware) {
292
+ // Excluir SOLO rutas de webhooks externos que firman su propia request.
293
+ $middleware->validateCsrfTokens(except: [
294
+ 'stripe/webhook',
295
+ 'webhooks/*',
296
+ ]);
297
+ })
298
+ ->create();
299
+ ```
300
+
301
+ ```blade
302
+ {{-- Formularios web: incluir SIEMPRE el token. --}}
303
+ <form method="POST" action="{{ route('posts.store') }}">
304
+ @csrf
305
+ {{-- ... --}}
306
+ </form>
307
+ ```
308
+
309
+ Reglas:
310
+
311
+ - No deshabilites CSRF globalmente. Excluye solo endpoints concretos de webhooks que validan firma propia.
312
+ - Las APIs con Sanctum por **token** (header `Authorization: Bearer`) no necesitan CSRF; las SPA de Sanctum por **cookie** sí dependen del flujo CSRF de Sanctum (`/sanctum/csrf-cookie`).
313
+
314
+ ---
315
+
316
+ ## 6. Rate limiting — `RateLimiter` y `throttle`
317
+
318
+ Sin rate limiting, login y endpoints de IA quedan expuestos a brute force y abuso de costo.
319
+
320
+ ```php
321
+ // app/Providers/AppServiceProvider.php — boot()
322
+ use Illuminate\Cache\RateLimiting\Limit;
323
+ use Illuminate\Http\Request;
324
+ use Illuminate\Support\Facades\RateLimiter;
325
+
326
+ public function boot(): void
327
+ {
328
+ // Límite por usuario autenticado, con fallback a IP.
329
+ RateLimiter::for('api', function (Request $request) {
330
+ return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
331
+ });
332
+
333
+ // Login: agresivo y por (email + IP) para frenar credential stuffing.
334
+ RateLimiter::for('login', function (Request $request) {
335
+ return Limit::perMinute(5)->by($request->input('email') . '|' . $request->ip());
336
+ });
337
+ }
338
+ ```
339
+
340
+ ```php
341
+ // routes/api.php — aplicar el limiter nombrado.
342
+ Route::middleware(['throttle:api'])->group(function () {
343
+ Route::apiResource('posts', PostController::class);
344
+ });
345
+
346
+ // Login con su propio limiter.
347
+ Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:login');
348
+
349
+ // Throttle inline simple: 100 requests por minuto.
350
+ Route::get('/search', SearchController::class)->middleware('throttle:100,1');
351
+ ```
352
+
353
+ Regla: aplica `throttle` a TODA ruta de auth (login, registro, reset password, verificación)
354
+ y a cualquier endpoint que dispare costo (IA, envío de email/SMS, exportaciones).
355
+
356
+ ---
357
+
358
+ ## 7. Inyección SQL — bindings, nunca concatenación
359
+
360
+ Eloquent y el Query Builder usan bindings por defecto y son seguros. El peligro aparece con
361
+ `DB::raw`, `whereRaw`, `orderByRaw`, `selectRaw` cuando metes input del usuario sin bindings.
362
+
363
+ ```php
364
+ use Illuminate\Support\Facades\DB;
365
+
366
+ // PELIGRO — concatenación directa de input. NUNCA:
367
+ $users = DB::select("SELECT * FROM users WHERE email = '" . $request->email . "'");
368
+ User::whereRaw("name = '" . $request->name . "'")->get();
369
+
370
+ // CORRECTO — bindings parametrizados:
371
+ $users = DB::select('SELECT * FROM users WHERE email = ?', [$request->email]);
372
+ User::whereRaw('name = ?', [$request->name])->get();
373
+
374
+ // Mejor aún — query builder de alto nivel (binding implícito):
375
+ User::where('email', $request->email)->get();
376
+ ```
377
+
378
+ ```php
379
+ // orderBy con columna dinámica: NUNCA pases el input directo a orderByRaw.
380
+ // Valida contra una allowlist de columnas permitidas.
381
+ $sortable = ['created_at', 'title', 'status'];
382
+ $column = in_array($request->input('sort'), $sortable, true)
383
+ ? $request->input('sort')
384
+ : 'created_at';
385
+
386
+ Post::orderBy($column)->paginate();
387
+ ```
388
+
389
+ Reglas:
390
+
391
+ - Nombres de columna/tabla NO pueden ir como binding (un `?` solo liga valores). Para columnas
392
+ dinámicas, valida contra una allowlist; nunca interpoles el string del usuario.
393
+ - `DB::raw` con input de usuario es la causa #1 de SQLi en Laravel. Si no puedes evitarlo, usa
394
+ el segundo argumento de bindings.
395
+
396
+ ---
397
+
398
+ ## 8. XSS — Blade `{{ }}` vs `{!! !!}`
399
+
400
+ ```blade
401
+ {{-- {{ }} escapa HTML automáticamente (htmlspecialchars). SEGURO por defecto. --}}
402
+ <h1>{{ $post->title }}</h1>
403
+ <p>Hola, {{ $user->name }}</p>
404
+
405
+ {{-- {!! !!} imprime HTML SIN escapar. Es la puerta directa a XSS. --}}
406
+ {{-- NUNCA con datos de usuario sin sanitizar. --}}
407
+ <div>{!! $userBio !!}</div> {{-- PELIGRO si $userBio viene del usuario --}}
408
+ ```
409
+
410
+ Si necesitas renderizar HTML provisto por usuarios (p. ej. contenido de un editor rich-text),
411
+ sanitízalo en el servidor con una allowlist antes de guardarlo o mostrarlo:
412
+
413
+ ```bash
414
+ composer require mews/purifier # HTML Purifier para Laravel
415
+ ```
416
+
417
+ ```php
418
+ // Sanitiza antes de persistir; solo entonces es defendible usar {!! !!}.
419
+ $post->body = clean($request->input('body')); // allowlist de tags/atributos seguros
420
+ ```
421
+
422
+ Reglas:
423
+
424
+ - `{{ }}` siempre, salvo que tengas una razón explícita y el contenido esté sanitizado.
425
+ - En atributos y URLs, valida esquemas: bloquea `javascript:` en `href`/`src`.
426
+ - En JSON embebido en Blade usa `@json($data)` (escapa correctamente para `<script>`).
427
+
428
+ ---
429
+
430
+ ## 9. Secrets — `.env` / `config`, nunca hardcodeados
431
+
432
+ ```php
433
+ // PELIGRO — clave hardcodeada en código que va a git:
434
+ $client = new StripeClient('sk_live_51H...');
435
+
436
+ // CORRECTO — el secret vive en .env y se lee vía config (cacheable).
437
+ // .env: STRIPE_SECRET=sk_live_...
438
+ // config/services.php: 'stripe' => ['secret' => env('STRIPE_SECRET')],
439
+ $client = new StripeClient(config('services.stripe.secret'));
440
+ ```
441
+
442
+ Reglas:
443
+
444
+ - Nunca llames `env()` fuera de archivos `config/*`. Con `config:cache` en producción, `env()`
445
+ devuelve `null` en runtime. Lee siempre vía `config('...')`.
446
+ - `.env` jamás se commitea (debe estar en `.gitignore`); commitea solo `.env.example` con placeholders.
447
+ - Rota cualquier secret que haya tocado un commit o un log, aunque lo hayas borrado después.
448
+
449
+ ---
450
+
451
+ ## 10. Cifrado de datos sensibles — `Crypt`
452
+
453
+ ```php
454
+ use Illuminate\Support\Facades\Crypt;
455
+
456
+ // Cifrado simétrico autenticado con APP_KEY (AES-256-GCM).
457
+ $encrypted = Crypt::encryptString($apiTokenDeTercero);
458
+ $plain = Crypt::decryptString($encrypted);
459
+ ```
460
+
461
+ ```php
462
+ // Mejor: castea el atributo como 'encrypted' para cifrar/descifrar transparente.
463
+ protected function casts(): array
464
+ {
465
+ return [
466
+ 'oauth_token' => 'encrypted',
467
+ 'recovery_codes' => 'encrypted:array',
468
+ ];
469
+ }
470
+ ```
471
+
472
+ Reglas:
473
+
474
+ - Passwords NO se cifran, se **hashean**: `Hash::make()` / `Hash::check()` (bcrypt/argon2). Cifrar
475
+ un password es un error (es reversible).
476
+ - `Crypt` depende de `APP_KEY`. Si rotas la key, los datos cifrados con la anterior dejan de
477
+ descifrarse: planifica re-cifrado.
478
+
479
+ ---
480
+
481
+ ## 11. HTTPS y headers de seguridad
482
+
483
+ ```php
484
+ // app/Providers/AppServiceProvider.php — boot()
485
+ use Illuminate\Support\Facades\URL;
486
+
487
+ public function boot(): void
488
+ {
489
+ // Fuerza generación de URLs https en producción (detrás de proxy/load balancer).
490
+ if ($this->app->isProduction()) {
491
+ URL::forceScheme('https');
492
+ }
493
+ }
494
+ ```
495
+
496
+ Confía en el proxy solo de forma explícita (TrustProxies se configura en `bootstrap/app.php`):
497
+
498
+ ```php
499
+ // bootstrap/app.php
500
+ ->withMiddleware(function (Middleware $middleware) {
501
+ $middleware->trustProxies(at: '*', headers:
502
+ Illuminate\Http\Request::HEADER_X_FORWARDED_FOR |
503
+ Illuminate\Http\Request::HEADER_X_FORWARDED_PROTO
504
+ );
505
+ })
506
+ ```
507
+
508
+ Headers de seguridad vía un middleware propio (registrado en el grupo `web`):
509
+
510
+ ```bash
511
+ php artisan make:middleware SecurityHeaders
512
+ ```
513
+
514
+ ```php
515
+ // app/Http/Middleware/SecurityHeaders.php
516
+ use Closure;
517
+ use Illuminate\Http\Request;
518
+ use Symfony\Component\HttpFoundation\Response;
519
+
520
+ public function handle(Request $request, Closure $next): Response
521
+ {
522
+ $response = $next($request);
523
+
524
+ $response->headers->set('X-Content-Type-Options', 'nosniff');
525
+ $response->headers->set('X-Frame-Options', 'DENY');
526
+ $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
527
+ $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
528
+ // Content-Security-Policy: define una política acorde a tu frontend.
529
+ $response->headers->set('Content-Security-Policy', "default-src 'self'");
530
+
531
+ return $response;
532
+ }
533
+ ```
534
+
535
+ ```php
536
+ // bootstrap/app.php — añadir al grupo web.
537
+ ->withMiddleware(function (Middleware $middleware) {
538
+ $middleware->web(append: [\App\Http\Middleware\SecurityHeaders::class]);
539
+ })
540
+ ```
541
+
542
+ ---
543
+
544
+ ## 12. Uploads seguros
545
+
546
+ ```php
547
+ // En el Form Request: valida tipo real, tamaño y dimensiones.
548
+ public function rules(): array
549
+ {
550
+ return [
551
+ 'avatar' => ['required', 'file', 'mimes:jpg,jpeg,png,webp', 'max:2048'], // KB
552
+ 'doc' => ['required', 'file', 'mimetypes:application/pdf', 'max:10240'],
553
+ ];
554
+ }
555
+ ```
556
+
557
+ ```php
558
+ // Guarda con nombre generado por Laravel (hash), nunca con el nombre del cliente.
559
+ // Por defecto va a storage/app/private (NO accesible públicamente).
560
+ $path = $request->file('avatar')->store('avatars'); // disco 'local' privado
561
+ // Para archivos públicos, usa el disco 'public' conscientemente:
562
+ // $path = $request->file('doc')->store('docs', 'public');
563
+ ```
564
+
565
+ Reglas:
566
+
567
+ - Valida `mimes`/`mimetypes` y `max` SIEMPRE. No confíes en la extensión ni en el `Content-Type` del cliente.
568
+ - Nunca uses `getClientOriginalName()` como nombre de archivo (path traversal / sobrescritura).
569
+ - Sirve archivos privados a través de un controller que verifica autorización, no por URL directa.
570
+ - Almacena uploads fuera del docroot; el directorio de subida no debe poder ejecutar PHP.
571
+
572
+ ---
573
+
574
+ ## 13. Deploy seguro
575
+
576
+ ```bash
577
+ # 1. Generar APP_KEY (una vez por entorno; sin ella Crypt/sesiones fallan).
578
+ php artisan key:generate
579
+
580
+ # 2. Cachear config/rutas/eventos (rápido y obliga a leer secrets vía config()).
581
+ php artisan config:cache
582
+ php artisan route:cache
583
+ php artisan event:cache
584
+
585
+ # 3. Optimizar autoloader de Composer sin dependencias de dev.
586
+ composer install --no-dev --optimize-autoloader
587
+
588
+ # 4. Migraciones sin prompt interactivo.
589
+ php artisan migrate --force
590
+ ```
591
+
592
+ Checklist `.env` de producción:
593
+
594
+ ```
595
+ ✓ APP_ENV=production
596
+ ✓ APP_DEBUG=false ← CRÍTICO: con true se exponen stacktraces, queries y secrets
597
+ ✓ APP_KEY=base64:... ← generada, no vacía
598
+ ✓ APP_URL=https://... ← https, no http
599
+ ✓ SESSION_SECURE_COOKIE=true ← cookies de sesión solo por https
600
+ ✓ LOG_LEVEL=error ← no loguear debug/info con PII en producción
601
+ ```
602
+
603
+ Reglas:
604
+
605
+ - `APP_DEBUG=false` en producción es no negociable. Con `true`, cualquier error 500 filtra
606
+ entorno, conexión a BD, fragmentos de código y a veces valores de `.env`.
607
+ - Nunca dejes `/telescope`, `/horizon`, `/_ignition` ni rutas de debug accesibles sin gate de auth.
608
+ - Verifica los runtime logs tras el deploy, no solo que el build pasó.
609
+
610
+ ---
611
+
612
+ ## Checklist OWASP por endpoint
613
+
614
+ Antes de mergear cualquier endpoint que toque auth, autorización o datos sensibles:
615
+
616
+ ```
617
+ AUTENTICACIÓN
618
+ ✓ El endpoint exige auth (auth:sanctum / sesión) si no es deliberadamente público
619
+ ✓ Sin token/sesión válida → 401, antes de cualquier lógica
620
+ ✓ La identidad sale del token/sesión, NO de headers que el cliente controla
621
+
622
+ AUTORIZACIÓN (Broken Access Control — #1 OWASP)
623
+ ✓ Hay una Policy/Gate que decide quién puede ejecutar la acción
624
+ ✓ Gate::authorize(...) o middleware can: se invoca en CADA método (no solo en uno)
625
+ ✓ IDOR cubierto: al cargar por ID se verifica ownership/permiso del recurso concreto
626
+
627
+ VALIDACIÓN E INPUT
628
+ ✓ Toda entrada pasa por un Form Request (authorize() + rules()), no validación inline
629
+ ✓ Se usa $request->validated()/safe(), nunca ->all() ni ->input() crudo al persistir
630
+ ✓ Mass assignment protegido con $fillable (allowlist); is_admin/user_id NO están ahí
631
+
632
+ INYECCIÓN
633
+ ✓ Sin DB::raw/whereRaw/orderByRaw con input concatenado; bindings o allowlist de columnas
634
+ ✓ Blade usa {{ }}; cualquier {!! !!} solo con contenido sanitizado en servidor
635
+
636
+ CONFIGURACIÓN Y EXPOSICIÓN
637
+ ✓ Rate limiting (throttle) en rutas de auth y de costo (IA, email/SMS, export)
638
+ ✓ Secrets vía config()/.env, nunca hardcodeados; .env fuera de git
639
+ ✓ Errores de producción no exponen stacktrace ni mensajes internos (APP_DEBUG=false)
640
+ ✓ Uploads: mimes/mimetypes + max validados, nombre generado, almacenamiento privado
641
+ ✓ CSRF activo en rutas web; excepciones solo para webhooks con firma propia
642
+ ```
643
+
644
+ Severidades (alineadas con el skill `security-audit`):
645
+
646
+ - **CRÍTICO**: endpoint sin auth, SQLi por `DB::raw`+input, IDOR sin Policy, `APP_DEBUG=true` en prod, secret hardcodeado en git.
647
+ - **ALTO**: bypass de autorización por rol, mass assignment de `is_admin`/`user_id`, `{!! !!}` con input de usuario.
648
+ - **MEDIO**: sin rate limiting en login/IA, CSRF deshabilitado de más, headers de seguridad ausentes.
649
+ - **BAJO**: cookies sin `Secure`, logs verbosos en producción, dependencias desactualizadas sin CVE activo.
650
+
651
+ ---
652
+
653
+ ## Relación con otros skills
654
+
655
+ - Complementa a `security-audit` (checklist agnóstico al stack) con la implementación concreta en Laravel.
656
+ - `new-feature` lo invoca cuando la feature toca auth, autorización o datos sensibles.
657
+ - Lo puede invocar el agente `forge-audit-specialist` o un reviewer durante un PR.
658
+ - Standalone: no depende de otros skills para ejecutarse.