A06: Insecure Design (Blog)
What is insecure design?
Insecure Design is when security rules are missing at the use-case level.
The implementation can be "correct" while the workflow is still exploitable because abuse cases, constraints, and invariants were never designed.
Typical Laravel failure (blog)
- Building features first, security later.
- No abuse cases.
- No ownership/visibility model.
- No workflow constraints for critical operations (publishing posts).
- No moderation/rate-limits for comments.
- No approval or dual-control for dangerous actions.
Impact
- Business logic abuse (the attacker uses your intended workflow against you).
- Draft/private content exposure when visibility is not modeled.
- Unauthorized critical state changes (payouts, role changes, ownership transfer).
- No forensic trail when auditing is missing.
Laravel 12 remediation
- Design security rules before coding endpoints.
- Model abuse cases in the use case itself.
- Use Actions as use-case boundaries.
- For critical Actions, require invariant checks:
- actor authorized
- visibility/ownership constraints
- state transition allowed
- audit event emitted
- Introduce rate limits, idempotency, approval flows, and immutable audit trails where required.
Concrete fix
Treat authorization + invariants as the first lines of your Action (publish flow):
use Illuminate\Support\Facades\Gate; final class PublishPostAction{ public function handle(User $actor, Post $post): void { // invariant: actor authorized to write posts Gate::authorize('update', $post); // invariant: allowed state transition abort_unless($post->status === 'draft', 422); $post->update(['status' => 'published']); // invariant: audit event emitted // event(new PostPublished($post->id, $actor->id)); }}
Design pattern angle
Use an Action as the boundary, and enforce invariants via a small pipeline / decorator chain. This keeps critical checks consistent across use cases (auth, ownership, state machine, audit).
interface Invariant{ public function check(array $context): void;} final class EnsureDraftStatus implements Invariant{ public function check(array $context): void { /** @var Post $post */ $post = $context['post']; abort_unless($post->status === 'draft', 422); }} final class InvariantRunner{ /** @param array<int, Invariant> $invariants */ public function __construct(private array $invariants) {} public function run(array $context): void { foreach ($this->invariants as $invariant) { $invariant->check($context); } }} // Usage inside an Action$runner = new InvariantRunner([ new EnsureDraftStatus(), // new EnsureAuditEnabled(),]); $runner->run([ 'actor' => $actor, 'post' => $post,]); // continue with the use case