Fix missing object-level authorization (IDOR) in PHP and Laravel
================================================================

Build one query that holds the rule, from $request->user()'s tenant and role,
never from request data: Invoice::where('tenant_id', $user->tenant_id),
narrowed to owner_id for members. Start every detail, list and export query
from it. Answer 404 when it finds nothing, and refuse an export when any
requested ID is out of scope.

Page: https://softwaresecurity.com/fix/missing-authorization/php-laravel/

What is in this archive
-----------------------
missing-authorization/php-laravel/   the project: three implementations of the same code and one test suite
  app/Invoices/VulnerableInvoiceAccess.php   (vulnerable)
  app/Invoices/FixedInvoiceAccess.php   (fixed)
  app/Invoices/WrongInvoiceAccess.php   (a plausible fix that still fails)
run-all.mjs   runs the tests in all three modes and checks the results (Node.js and Docker)
LICENSE-CODE.txt

Run it
------
  Tested with PHP 8.5.11 (CLI). run-all.mjs needs Node.js 22 or later. Supported
  versions: https://softwaresecurity.com/fix/versions/

  From the missing-authorization/php-laravel folder, install the dependencies
  once (this needs network access):

    composer install --no-interaction --no-progress

  Then run the tests. IMPL chooses the implementation the tests load:

    # every test passes
    IMPL=fixed vendor/bin/phpunit --log-junit reports/junit.xml
    # an exploit test fails
    IMPL=vulnerable vendor/bin/phpunit --log-junit reports/junit.xml
    # a test fails
    IMPL=wrong vendor/bin/phpunit --log-junit reports/junit.xml

  Without PHP installed, Docker runs the same commands in the image the card was
  verified in, pinned by its digest (run them from the
  missing-authorization/php-laravel folder; the named volumes keep the
  dependency and build caches between runs, so the test runs need no network):

    docker run --rm --init \
      -v "$PWD":/card -w /card \
      -v fix-laravel-composer-cache:/tmp/cache \
      composer:2.10.3@sha256:af98f42dfff7c68ba8d53c2164fd9fde1087b7d449514baa38c418b1f6bc4bac \
      sh -c 'composer install --no-interaction --no-progress'

    # every test passes
    docker run --rm --init --network=none \
      -v "$PWD":/card -w /card \
      -v fix-laravel-composer-cache:/tmp/cache \
      -e IMPL=fixed \
      composer:2.10.3@sha256:af98f42dfff7c68ba8d53c2164fd9fde1087b7d449514baa38c418b1f6bc4bac \
      sh -c 'vendor/bin/phpunit --log-junit reports/junit.xml'
    # an exploit test fails
    docker run --rm --init --network=none \
      -v "$PWD":/card -w /card \
      -v fix-laravel-composer-cache:/tmp/cache \
      -e IMPL=vulnerable \
      composer:2.10.3@sha256:af98f42dfff7c68ba8d53c2164fd9fde1087b7d449514baa38c418b1f6bc4bac \
      sh -c 'vendor/bin/phpunit --log-junit reports/junit.xml'
    # a test fails
    docker run --rm --init --network=none \
      -v "$PWD":/card -w /card \
      -v fix-laravel-composer-cache:/tmp/cache \
      -e IMPL=wrong \
      composer:2.10.3@sha256:af98f42dfff7c68ba8d53c2164fd9fde1087b7d449514baa38c418b1f6bc4bac \
      sh -c 'vendor/bin/phpunit --log-junit reports/junit.xml'

  The JUnit report is written to reports/junit.xml. To run all three modes and
  check the results in one step, run the harness from this top folder (it needs
  only Node.js and Docker: it runs the commands in the container; --install runs
  the install step first):

    node run-all.mjs --install missing-authorization/php-laravel
    node run-all.mjs missing-authorization/php-laravel

Verified
--------
  11 October 2026 on Alpine Linux 3.24 in the composer:2.10.3 image (Docker
  29.4.3 on macOS 27.0, arm64), with php 8.5.11 (CLI), laravel/framework
  13.34.0, phpunit/phpunit 13.4.0, composer 2.10.3, semgrep 1.180.0, psalm
  7.0.0-beta24 with psalm/plugin-laravel 4.16.6. Versions and results hold for
  that date: run the tests with your own versions before relying on the fix.

Licence
-------
  The code is MIT licensed (LICENSE-CODE.txt). The page text is CC BY 4.0.
