Skip Content

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

References