Skip to content

Cache only the last getAllowedMethods() lookup - #3468

Merged
akrabat merged 3 commits into
slimphp:4.xfrom
kaii-k:fix/sec02-bound-allowed-methods-cache
Aug 29, 2026
Merged

Cache only the last getAllowedMethods() lookup#3468
akrabat merged 3 commits into
slimphp:4.xfrom
kaii-k:fix/sec02-bound-allowed-methods-cache

Conversation

@kaii-k

@kaii-k kaii-k commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

FastRouteDispatcher::getAllowedMethods() memoizes its result per requested URI in an instance property with no cap, TTL, or eviction. On dispatchers that outlive a single request — persistent-worker runtimes (FrankenPHP worker mode, RoadRunner, Swoole) or any long-lived process reusing one App/Dispatcher instance (queue workers, CLI daemons, test/load harnesses) — an attacker can grow this cache without bound by requesting distinct, unmatched URIs, exhausting process memory over time.

This caps the cache at 1000 entries with oldest-first eviction. It's a no-op for the standard php-fpm-per-request model, since the dispatcher never survives past one request there.

Reported privately via security@slimframework.com — this PR is the fix discussed there.

Test plan

  • Added testGetAllowedMethodsCacheIsBounded reproducing unbounded growth against the cap.
  • Full test suite passes (436 tests).
  • phpcs / phpstan clean on changed files.

FastRouteDispatcher::getAllowedMethods() memoizes its result per
requested URI in an instance property with no cap, TTL, or eviction.
On dispatchers that outlive a single request (persistent-worker
runtimes such as FrankenPHP worker mode, RoadRunner, Swoole, or any
long-lived process reusing one App/Dispatcher instance, e.g. a queue
worker or CLI daemon), an attacker can grow this cache without bound
by requesting distinct, unmatched URIs, exhausting process memory.

Cap the cache at 1000 entries with simple oldest-first eviction. This
is a no-op for the traditional php-fpm-per-request model, where the
dispatcher never survives past one request.
@akrabat akrabat self-assigned this Aug 26, 2026
@akrabat akrabat added this to the 4.16.0 milestone Aug 26, 2026
@coveralls

coveralls commented Aug 26, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 99.456% (+0.001%) from 99.455% — kaii-k:fix/sec02-bound-allowed-methods-cache into slimphp:4.x

@akrabat

akrabat commented Aug 26, 2026

Copy link
Copy Markdown
Member

Looking at this, I'm not sure that we actually need the array of allowedMethods at all. We only actually need one URI and one list of allowed methods.

getAllowedMethods() only ever needs its most recent result: dispatch()
calls it once per invocation for the current URI, and across calls the
cache only pays off when the same URI repeats back-to-back. Replace the
capped/evicting map with a single (uri, methods) pair, which is bounded
by construction and removes the cap/eviction logic entirely.
@kaii-k

kaii-k commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Good point you're right, we don't need a history of URIs at all. getAllowedMethods() is only called once per dispatch() for the current URI, and across dispatches the cache only pays off when the same URI repeats back-to-back, so a single-entry memo covers that with no cap/eviction logic needed.

Pushed a commit replacing the bounded map with a single $allowedMethodsUri / $allowedMethods pair (last-URI memo). It's bounded by construction rather than by a configurable cap, which is simpler and removes the MAX_ALLOWED_METHODS_CACHE_SIZE constant and eviction branch entirely. Updated the regression test accordingly (asserts the memo holds only the most recent URI after many distinct lookups, plus a test that re-requesting an earlier URI recomputes rather than returning a stale hit). Full suite still green (437 tests), phpcs/phpstan clean.

@akrabat akrabat changed the title Bound FastRouteDispatcher::getAllowedMethods() cache size (fixes unbounded memory growth under persistent-worker runtimes) Cache only the last getAllowedMethods() lookup Aug 29, 2026

@akrabat akrabat left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update.

@akrabat
akrabat merged commit df489a9 into slimphp:4.x Aug 29, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants