start-vibing-stacks 2.16.0 → 2.17.0
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.
- package/package.json +1 -1
- package/stacks/php/skills/composer-workflow/SKILL.md +118 -34
- package/stacks/php/skills/external-api-patterns/SKILL.md +171 -89
- package/stacks/php/skills/laravel-octane/SKILL.md +74 -2
- package/stacks/php/skills/mariadb-octane/SKILL.md +96 -4
- package/stacks/php/skills/php-patterns/SKILL.md +98 -5
- package/stacks/php/skills/phpstan-analysis/SKILL.md +136 -30
- package/stacks/php/skills/phpunit-testing/SKILL.md +247 -61
- package/stacks/php/skills/security-scan-php/SKILL.md +96 -3
|
@@ -1,40 +1,207 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: phpunit-testing
|
|
3
|
-
version:
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: "PHP testing for 2026 — covers PHPUnit 12 (released 2025, foundation for Pest 4) and Pest 4 (March 2026: 20-30% faster, browser testing via Playwright, visual regression, smoke testing, sharding for CI parallelization, mutation testing). Includes Unit / Feature / Browser / Architecture test layout, AAA pattern, data providers / datasets, mocking, fixtures, dataset sharding, coverage thresholds. Invoke when authoring tests, choosing PHPUnit vs Pest, configuring sharding/parallel, or wiring CI test stages."
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
#
|
|
7
|
+
# PHP Testing — PHPUnit 12 & Pest 4 (2026)
|
|
8
|
+
|
|
9
|
+
**ALWAYS invoke when writing tests, choosing the framework, or wiring CI sharding.**
|
|
10
|
+
|
|
11
|
+
> Pest 4 (Mar 10, 2026) is built on PHPUnit 12. New projects in 2026: prefer **Pest 4** for ergonomics and built-in browser/visual testing; stay on **PHPUnit 12** if your team has muscle memory or you need raw assertion control.
|
|
12
|
+
|
|
13
|
+
## Framework Choice
|
|
14
|
+
|
|
15
|
+
| Need | Pick |
|
|
16
|
+
|---|---|
|
|
17
|
+
| New Laravel app, want fluent expectation API + browser tests | **Pest 4** |
|
|
18
|
+
| Plain PHP library, prefer xUnit-classic | **PHPUnit 12** |
|
|
19
|
+
| Big legacy PHPUnit suite | **PHPUnit 12** (don't migrate just for migration's sake) |
|
|
20
|
+
| Need visual regression / Playwright in PHP | **Pest 4** (built-in) |
|
|
21
|
+
|
|
22
|
+
Both share the same engine — assertions, mocks, runners are interoperable through Pest's PHPUnit base. Pest is a thin DSL on top.
|
|
7
23
|
|
|
8
24
|
## Setup
|
|
9
25
|
|
|
26
|
+
### Pest 4 (recommended for new Laravel projects)
|
|
27
|
+
|
|
10
28
|
```bash
|
|
11
|
-
composer require --dev
|
|
29
|
+
composer require --dev pestphp/pest "^4.0" # PHP 8.3+
|
|
30
|
+
composer require --dev pestphp/pest-plugin-laravel "^4.0"
|
|
31
|
+
./vendor/bin/pest --init
|
|
12
32
|
```
|
|
13
33
|
|
|
14
|
-
|
|
34
|
+
### PHPUnit 12
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
composer require --dev phpunit/phpunit "^12.0"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## File Layout
|
|
15
41
|
|
|
16
42
|
```
|
|
17
43
|
tests/
|
|
44
|
+
├── Pest.php # Pest config (test dirs, datasets, helpers)
|
|
45
|
+
├── TestCase.php # Shared base
|
|
18
46
|
├── Unit/
|
|
19
|
-
│ ├── Services/
|
|
20
|
-
│
|
|
21
|
-
│ └── Helpers/
|
|
22
|
-
│ └── StringHelperTest.php
|
|
47
|
+
│ ├── Services/UserServiceTest.php
|
|
48
|
+
│ └── Helpers/StringHelperTest.php
|
|
23
49
|
├── Feature/
|
|
24
|
-
│ └── Api/
|
|
25
|
-
|
|
26
|
-
└──
|
|
50
|
+
│ └── Api/UserApiTest.php
|
|
51
|
+
├── Browser/ # Pest 4 — Playwright-backed
|
|
52
|
+
│ └── LoginFlowTest.php
|
|
53
|
+
└── Arch/ # Pest — architecture rules
|
|
54
|
+
└── LayerBoundariesTest.php
|
|
55
|
+
phpunit.xml # used by both Pest & PHPUnit
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## `phpunit.xml` — production-grade baseline
|
|
59
|
+
|
|
60
|
+
```xml
|
|
61
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
62
|
+
<phpunit
|
|
63
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
64
|
+
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
|
65
|
+
bootstrap="vendor/autoload.php"
|
|
66
|
+
colors="true"
|
|
67
|
+
cacheDirectory=".phpunit.cache"
|
|
68
|
+
requireCoverageMetadata="true"
|
|
69
|
+
beStrictAboutCoverageMetadata="true"
|
|
70
|
+
beStrictAboutOutputDuringTests="true"
|
|
71
|
+
failOnRisky="true"
|
|
72
|
+
failOnWarning="true"
|
|
73
|
+
executionOrder="random">
|
|
74
|
+
|
|
75
|
+
<testsuites>
|
|
76
|
+
<testsuite name="Unit">
|
|
77
|
+
<directory>tests/Unit</directory>
|
|
78
|
+
</testsuite>
|
|
79
|
+
<testsuite name="Feature">
|
|
80
|
+
<directory>tests/Feature</directory>
|
|
81
|
+
</testsuite>
|
|
82
|
+
<testsuite name="Browser">
|
|
83
|
+
<directory>tests/Browser</directory>
|
|
84
|
+
</testsuite>
|
|
85
|
+
</testsuites>
|
|
86
|
+
|
|
87
|
+
<source>
|
|
88
|
+
<include>
|
|
89
|
+
<directory>app</directory>
|
|
90
|
+
</include>
|
|
91
|
+
</source>
|
|
92
|
+
|
|
93
|
+
<coverage includeUncoveredFiles="true">
|
|
94
|
+
<report>
|
|
95
|
+
<clover outputFile="coverage.xml"/>
|
|
96
|
+
<text outputFile="php://stdout" showOnlySummary="true"/>
|
|
97
|
+
</report>
|
|
98
|
+
</coverage>
|
|
99
|
+
|
|
100
|
+
<php>
|
|
101
|
+
<env name="APP_ENV" value="testing"/>
|
|
102
|
+
<env name="DB_CONNECTION" value="sqlite"/>
|
|
103
|
+
<env name="DB_DATABASE" value=":memory:"/>
|
|
104
|
+
</php>
|
|
105
|
+
</phpunit>
|
|
27
106
|
```
|
|
28
107
|
|
|
29
|
-
##
|
|
108
|
+
## Pest 4 — patterns
|
|
109
|
+
|
|
110
|
+
### Unit + Feature
|
|
30
111
|
|
|
31
112
|
```php
|
|
32
|
-
|
|
113
|
+
// tests/Unit/Services/UserServiceTest.php
|
|
114
|
+
use App\Services\UserService;
|
|
115
|
+
|
|
116
|
+
beforeEach(fn () => $this->service = new UserService);
|
|
117
|
+
|
|
118
|
+
it('creates a user with valid data', function () {
|
|
119
|
+
$user = $this->service->create(['name' => 'John', 'email' => 'john@test.com']);
|
|
120
|
+
|
|
121
|
+
expect($user)
|
|
122
|
+
->name->toBe('John')
|
|
123
|
+
->email->toBe('john@test.com');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('rejects invalid email', function () {
|
|
127
|
+
$this->service->create(['name' => 'John', 'email' => 'invalid']);
|
|
128
|
+
})->throws(InvalidArgumentException::class, 'Invalid email');
|
|
129
|
+
|
|
130
|
+
dataset('invalid_users', [
|
|
131
|
+
'empty name' => [['name' => '', 'email' => 'a@b.com'], 'Name required'],
|
|
132
|
+
'empty email' => [['name' => 'John', 'email' => ''], 'Email required'],
|
|
133
|
+
'no data' => [[], 'Name required'],
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
it('rejects invalid data', function (array $data, string $expected) {
|
|
137
|
+
$this->service->create($data);
|
|
138
|
+
})->with('invalid_users')->throws(InvalidArgumentException::class);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Browser test (Pest 4 — Playwright-backed)
|
|
142
|
+
|
|
143
|
+
```php
|
|
144
|
+
// tests/Browser/LoginFlowTest.php
|
|
145
|
+
use function Pest\Browser\visit;
|
|
146
|
+
|
|
147
|
+
it('logs the user in', function () {
|
|
148
|
+
visit('/login')
|
|
149
|
+
->fill('email', 'user@test.com')
|
|
150
|
+
->fill('password', 'secret')
|
|
151
|
+
->press('Sign in')
|
|
152
|
+
->assertPathIs('/dashboard')
|
|
153
|
+
->assertSee('Welcome back');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Multi-viewport smoke
|
|
157
|
+
it('login page is responsive', function (string $device) {
|
|
158
|
+
visit('/login')
|
|
159
|
+
->onDevice($device) // 'iphone-14', 'ipad', 'desktop-1280'
|
|
160
|
+
->assertNoConsoleErrors()
|
|
161
|
+
->assertNoBrokenLinks()
|
|
162
|
+
->screenshot();
|
|
163
|
+
})->with(['iphone-14', 'ipad', 'desktop-1280']);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Architecture test (Pest)
|
|
33
167
|
|
|
168
|
+
```php
|
|
169
|
+
// tests/Arch/LayerBoundariesTest.php
|
|
170
|
+
arch('controllers do not call DB directly')
|
|
171
|
+
->expect('App\Http\Controllers')
|
|
172
|
+
->not->toUse(['Illuminate\Support\Facades\DB']);
|
|
173
|
+
|
|
174
|
+
arch('domain layer is framework-free')
|
|
175
|
+
->expect('App\Domain')
|
|
176
|
+
->not->toUse([
|
|
177
|
+
'Illuminate\Database',
|
|
178
|
+
'Illuminate\Http',
|
|
179
|
+
'Illuminate\Support\Facades',
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
arch('strict types everywhere')
|
|
183
|
+
->expect('App')
|
|
184
|
+
->toUseStrictTypes();
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Mutation testing
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
./vendor/bin/pest --mutate --covered-only
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Pest mutates your code and re-runs tests; surviving mutants reveal weak assertions.
|
|
194
|
+
|
|
195
|
+
## PHPUnit 12 — same domain, classic style
|
|
196
|
+
|
|
197
|
+
```php
|
|
198
|
+
<?php
|
|
34
199
|
declare(strict_types=1);
|
|
35
200
|
|
|
36
201
|
namespace Tests\Unit\Services;
|
|
37
202
|
|
|
203
|
+
use PHPUnit\Framework\Attributes\DataProvider;
|
|
204
|
+
use PHPUnit\Framework\Attributes\Test;
|
|
38
205
|
use PHPUnit\Framework\TestCase;
|
|
39
206
|
use App\Services\UserService;
|
|
40
207
|
|
|
@@ -48,80 +215,99 @@ final class UserServiceTest extends TestCase
|
|
|
48
215
|
$this->service = new UserService();
|
|
49
216
|
}
|
|
50
217
|
|
|
51
|
-
|
|
218
|
+
#[Test]
|
|
219
|
+
public function it_creates_a_user_with_valid_data(): void
|
|
52
220
|
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Act
|
|
57
|
-
$user = $this->service->create($data);
|
|
58
|
-
|
|
59
|
-
// Assert
|
|
60
|
-
$this->assertSame('John', $user->name);
|
|
61
|
-
$this->assertSame('john@test.com', $user->email);
|
|
221
|
+
$user = $this->service->create(['name' => 'John', 'email' => 'john@test.com']);
|
|
222
|
+
self::assertSame('John', $user->name);
|
|
223
|
+
self::assertSame('john@test.com', $user->email);
|
|
62
224
|
}
|
|
63
225
|
|
|
64
|
-
|
|
226
|
+
#[Test]
|
|
227
|
+
#[DataProvider('invalidDataProvider')]
|
|
228
|
+
public function it_rejects_invalid_data(array $data, string $expected): void
|
|
65
229
|
{
|
|
66
230
|
$this->expectException(\InvalidArgumentException::class);
|
|
67
|
-
$this->expectExceptionMessage(
|
|
68
|
-
|
|
69
|
-
$this->service->create(['name' => 'John', 'email' => 'invalid']);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* @dataProvider invalidDataProvider
|
|
74
|
-
*/
|
|
75
|
-
public function testCreateUserRejectsInvalidData(array $data, string $expectedError): void
|
|
76
|
-
{
|
|
77
|
-
$this->expectException(\InvalidArgumentException::class);
|
|
78
|
-
$this->expectExceptionMessage($expectedError);
|
|
79
|
-
|
|
231
|
+
$this->expectExceptionMessage($expected);
|
|
80
232
|
$this->service->create($data);
|
|
81
233
|
}
|
|
82
234
|
|
|
83
235
|
public static function invalidDataProvider(): array
|
|
84
236
|
{
|
|
85
237
|
return [
|
|
86
|
-
'empty name'
|
|
87
|
-
'empty email' => [['name' => 'John', 'email' => ''],
|
|
88
|
-
'
|
|
238
|
+
'empty name' => [['name' => '', 'email' => 'a@b.com'], 'Name required'],
|
|
239
|
+
'empty email' => [['name' => 'John', 'email' => ''], 'Email required'],
|
|
240
|
+
'no data' => [[], 'Name required'],
|
|
89
241
|
];
|
|
90
242
|
}
|
|
91
243
|
}
|
|
92
244
|
```
|
|
93
245
|
|
|
246
|
+
> PHPUnit 12 deprecated `/** @test */` and `/** @dataProvider */` annotations — use the `#[Test]` and `#[DataProvider]` attributes shown above.
|
|
247
|
+
|
|
94
248
|
## Mocking
|
|
95
249
|
|
|
96
250
|
```php
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
->willReturn(true);
|
|
251
|
+
// PHPUnit
|
|
252
|
+
$repo = $this->createMock(UserRepository::class);
|
|
253
|
+
$repo->expects($this->once())
|
|
254
|
+
->method('save')
|
|
255
|
+
->with($this->isInstanceOf(User::class))
|
|
256
|
+
->willReturn(true);
|
|
104
257
|
|
|
105
|
-
|
|
106
|
-
|
|
258
|
+
// Pest — same engine, fluent
|
|
259
|
+
$repo = mock(UserRepository::class)
|
|
260
|
+
->shouldReceive('save')->once()->andReturn(true)
|
|
261
|
+
->getMock();
|
|
262
|
+
```
|
|
107
263
|
|
|
108
|
-
|
|
109
|
-
|
|
264
|
+
For HTTP: `Http::fake()` (Laravel) or Saloon's `MockClient` — see `external-api-patterns`.
|
|
265
|
+
|
|
266
|
+
## Sharding for CI *(Pest 4 / PHPUnit 12)*
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
# Split a slow suite across 4 runners
|
|
270
|
+
./vendor/bin/pest --parallel --shard=1/4
|
|
271
|
+
./vendor/bin/pest --parallel --shard=2/4
|
|
272
|
+
./vendor/bin/pest --parallel --shard=3/4
|
|
273
|
+
./vendor/bin/pest --parallel --shard=4/4
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
```yaml
|
|
277
|
+
# .github/workflows/ci.yml
|
|
278
|
+
strategy:
|
|
279
|
+
matrix:
|
|
280
|
+
shardIndex: [1, 2, 3, 4]
|
|
281
|
+
shardTotal: [4]
|
|
282
|
+
steps:
|
|
283
|
+
- run: ./vendor/bin/pest --parallel --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
|
110
284
|
```
|
|
111
285
|
|
|
112
|
-
##
|
|
286
|
+
## Coverage Thresholds
|
|
113
287
|
|
|
114
288
|
```bash
|
|
115
|
-
|
|
116
|
-
vendor/bin/
|
|
117
|
-
vendor/bin/phpunit --
|
|
118
|
-
vendor/bin/phpunit --coverage-text # With coverage
|
|
289
|
+
# Fail under 70%
|
|
290
|
+
./vendor/bin/pest --coverage --min=70
|
|
291
|
+
./vendor/bin/phpunit --coverage-text --min=70
|
|
119
292
|
```
|
|
120
293
|
|
|
294
|
+
Realistic 2026 baseline: **70% line coverage minimum**, 100% on Services / Domain layer, lower for Controllers (covered by feature tests anyway).
|
|
295
|
+
|
|
121
296
|
## Rules
|
|
122
297
|
|
|
123
|
-
1. **One
|
|
124
|
-
2. **Descriptive
|
|
125
|
-
3. **
|
|
126
|
-
4. **
|
|
127
|
-
5.
|
|
298
|
+
1. **One concept per test** — don't `@dataProvider` a test that mixes unrelated assertions
|
|
299
|
+
2. **Descriptive names** — `it_rejects_invalid_email`, not `testCreateUser2`
|
|
300
|
+
3. **Datasets / data providers** for input matrices (DRY assertions, named cases)
|
|
301
|
+
4. **`#[Test]` attribute, not `@test` PHPDoc** (PHPUnit 12 deprecated the latter)
|
|
302
|
+
5. **`setUp` for shared fresh state, not shared mutable state**
|
|
303
|
+
6. **Architecture tests on every Laravel project** — enforce layer rules in code, not docs
|
|
304
|
+
7. **Mutation testing on critical Services** quarterly — catches "fake green" suites
|
|
305
|
+
8. **Never `->skip()` / `->only()` in committed code**
|
|
306
|
+
9. **Browser tests use real browsers (Pest 4 Playwright)** — no jsdom hacks
|
|
307
|
+
|
|
308
|
+
## See Also
|
|
309
|
+
|
|
310
|
+
- `playwright-automation` (`_shared`) — same Playwright engine Pest 4 uses
|
|
311
|
+
- `quality-gate` — test step ordering
|
|
312
|
+
- `external-api-patterns` — `Http::fake()` / Saloon mocking patterns
|
|
313
|
+
- `phpstan-analysis` — typing that makes tests cheaper
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: security-scan-php
|
|
3
|
-
version:
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: "Laravel-specific security overlay on top of `security-baseline` (OWASP 2021 + 2025 deltas). Mass assignment, Sanctum/policies, Blade XSS, env() pitfalls in Octane, file upload validation, signed URLs, password hashing (Argon2id default since L10), CSRF on web routes, role-aware scoping, supply-chain hardening (composer audit + Roave Security Advisories + gitleaks 3-layer). Cross-references stay aligned with `security-baseline` §A01–§A10 anchors so the security-auditor agent can match. Invoke when reviewing any Laravel feature touching user data, auth, persistence, file IO, or external calls."
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
# Laravel Security Scan
|
|
7
|
+
# Laravel Security Scan (overlay on `security-baseline`)
|
|
8
|
+
|
|
9
|
+
> **This skill is a Laravel overlay.** The universal OWASP rules (with 2025 deltas — Software Supply Chain Failures, Mishandling Exceptional Conditions) live in `_shared/skills/security-baseline`. The §A01–§A10 anchors below match those (2021 numbering kept for security-auditor cross-references).
|
|
7
10
|
|
|
8
11
|
## OWASP Top 10 for Laravel
|
|
9
12
|
|
|
@@ -189,15 +192,105 @@ $user = $request->user();
|
|
|
189
192
|
- [ ] Sensitive headers set (X-Content-Type-Options, X-Frame-Options, CSP)
|
|
190
193
|
- [ ] Rate limiting on authentication endpoints
|
|
191
194
|
|
|
195
|
+
## Supply-Chain Hardening (OWASP 2025-A03)
|
|
196
|
+
|
|
197
|
+
Laravel apps inherit every CVE in the Composer tree. The build server is a target.
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
# Production install — never run third-party scripts on the build server
|
|
201
|
+
composer install --no-dev --no-scripts --optimize-autoloader --classmap-authoritative
|
|
202
|
+
|
|
203
|
+
# Audit (Composer 2.8+) — fail the PR on vulns OR abandoned packages
|
|
204
|
+
composer audit --abandoned=fail --ignore-severity=low
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
// composer.json — refuse to install any version with a known CVE
|
|
209
|
+
"require-dev": {
|
|
210
|
+
"roave/security-advisories": "dev-latest"
|
|
211
|
+
},
|
|
212
|
+
"config": {
|
|
213
|
+
"allow-plugins": {
|
|
214
|
+
"pestphp/pest-plugin": true
|
|
215
|
+
// Add new plugins explicitly. Default-deny.
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Layered secret detection (mirrors `secrets-management` skill):
|
|
221
|
+
|
|
222
|
+
```yaml
|
|
223
|
+
# .github/workflows/ci.yml
|
|
224
|
+
- uses: gitleaks/gitleaks-action@v2 # Layer 2 — CI scan
|
|
225
|
+
- run: composer audit --abandoned=fail # Composer-side audit
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Layer 1 = pre-commit gitleaks hook locally. Layer 3 = GitHub Push Protection at the org/repo. See `secrets-management` skill for full setup.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Mishandling Exceptional Conditions (OWASP 2025-A10) — Laravel-specific
|
|
233
|
+
|
|
234
|
+
The new 2025 category. Common Laravel mistakes:
|
|
235
|
+
|
|
236
|
+
| Anti-pattern | Why dangerous | Fix |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| `try { Gate::authorize(...) } catch { /* ignore */ }` | Authz fails open — caller proceeds as if allowed | Let it bubble; Laravel maps `AuthorizationException` to 403 |
|
|
239
|
+
| `Render::abort` returning a 500 with full stack trace | Leaks file paths, ORM internals, env hints | Set `APP_DEBUG=false` in prod; custom `app/Exceptions/Handler.php` returns generic problem+json |
|
|
240
|
+
| Webhook handler returns 500 on parse error | Provider retries forever, floods the queue | Verify signature first; on parse error log + return 200 (idempotently dropped) |
|
|
241
|
+
| Partial writes when transaction rolls back partially | Money charged but order not created | `DB::transaction()` closure (auto-rollback) — see `mariadb-octane` |
|
|
242
|
+
| `if (!config('feature.requireMfa')) return $user;` | Missing config = MFA bypassed | Default-deny: `if (config('feature.requireMfa', true) === false && app()->runningInConsole())` — and even then, gate behind explicit env |
|
|
243
|
+
| Octane uncaught exception leaves DB in TX | Next request inherits open TX → cascading failures | `try/finally { DB::rollBack() }` or `DB::transaction()` closure (see `mariadb-octane`) |
|
|
244
|
+
|
|
245
|
+
```php
|
|
246
|
+
// Correct webhook reception — fail closed on signature, fail open on parse
|
|
247
|
+
public function handle(Request $request): JsonResponse
|
|
248
|
+
{
|
|
249
|
+
try {
|
|
250
|
+
$event = \Stripe\Webhook::constructEvent(
|
|
251
|
+
$request->getContent(),
|
|
252
|
+
$request->header('Stripe-Signature'),
|
|
253
|
+
config('services.stripe.webhook_secret'),
|
|
254
|
+
);
|
|
255
|
+
} catch (\UnexpectedValueException | \Stripe\Exception\SignatureVerificationException $e) {
|
|
256
|
+
Log::warning('[stripe] invalid signature', ['err' => $e->getMessage()]);
|
|
257
|
+
return response()->json(['error' => 'invalid signature'], 400); // 4xx → provider stops retrying
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
ProcessStripeEvent::dispatch($event->type, $event->data->object);
|
|
262
|
+
} catch (\Throwable $e) {
|
|
263
|
+
Log::error('[stripe] dispatch failed', ['event' => $event->id, 'err' => $e->getMessage()]);
|
|
264
|
+
// Acknowledge anyway — we'll reconcile from the webhook log
|
|
265
|
+
}
|
|
266
|
+
return response()->json(['status' => 'received']);
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
192
272
|
## Sensitive Patterns (FORBIDDEN)
|
|
193
273
|
|
|
194
274
|
| Pattern | Risk |
|
|
195
275
|
|---------|------|
|
|
196
276
|
| `DB::raw()` with user input | SQL Injection |
|
|
197
277
|
| `{!! $userInput !!}` | XSS |
|
|
198
|
-
| `md5()` / `sha1()` for passwords | Weak hashing |
|
|
278
|
+
| `md5()` / `sha1()` for passwords | Weak hashing (Argon2id is the Laravel 10+ default) |
|
|
199
279
|
| Dynamic code execution | RCE |
|
|
200
280
|
| `unserialize()` on user data | Object injection |
|
|
201
281
|
| `$request->all()` without `$fillable` | Mass assignment |
|
|
202
282
|
| `env()` in runtime code | Null after config cache |
|
|
203
283
|
| Static user state in services | Data leaks in Octane |
|
|
284
|
+
| `composer install` with `--ignore-platform-reqs` in prod | Hides incompatible PHP/ext versions |
|
|
285
|
+
| `composer install` in prod **without** `--no-scripts` | Supply-chain RCE on package post-install hooks |
|
|
286
|
+
| Missing `roave/security-advisories` dev dep | Known CVE installs silently |
|
|
287
|
+
| `try { ... } catch { /* nothing */ }` around authz | 2025-A10 fail-open |
|
|
288
|
+
|
|
289
|
+
## See Also
|
|
290
|
+
|
|
291
|
+
- `_shared/skills/security-baseline` — universal §A01–§A10 (OWASP 2021 + 2025 deltas)
|
|
292
|
+
- `_shared/skills/secrets-management` — gitleaks 3-layer + OIDC + Roave
|
|
293
|
+
- `_shared/skills/observability` — log redaction
|
|
294
|
+
- `composer-workflow` — `audit` + `roave/security-advisories`
|
|
295
|
+
- `mariadb-octane` — transaction safety in long-lived workers
|
|
296
|
+
- `api-security` — Sanctum cookie SPA + CORS hardening
|