Skip to content

fix(laravel): don't let Kernel::handle hook throw on hostile method override#650

Open
hendrikheil wants to merge 4 commits into
open-telemetry:mainfrom
hendrikheil:fix/laravel-kernel-hook-suspicious-operation
Open

fix(laravel): don't let Kernel::handle hook throw on hostile method override#650
hendrikheil wants to merge 4 commits into
open-telemetry:mainfrom
hendrikheil:fix/laravel-kernel-hook-suspicious-operation

Conversation

@hendrikheil

@hendrikheil hendrikheil commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Which problem is this PR solving?

Illuminate\Http\Request::method()/getMethod() throws a SuspiciousOperationException when the request carries a malformed HTTP method override — e.g. _method=__construct or a X-HTTP-METHOD-OVERRIDE: __construct header, which is a fairly common vulnerability-scanner probe. getHost() has the same failure mode for a Host header that fails validation, and it's reached both directly (host()/getHost()) and indirectly via fullUrl() (used for the URL_FULL attribute).

The Kernel::handle pre hook calls all of these unguarded while building the span. Because this is a pre hook, it runs before the intercepted Kernel::handle() method body executes — i.e. before Laravel's own try { ... } catch (Throwable $e) around request handling is even reached. An uncaught throw here isn't caught by the application at all; the extension reports it as a raw PHP warning outside the normal response lifecycle (OpenTelemetry: pre hook threw exception, ... message=Invalid HTTP method override.), and the span for the request is silently dropped.

In a real deployment this surfaced as a FatalError: Cannot modify header information - headers already sent — the warning text got written to the response body before any headers were sent, so a later attempt to set a header (e.g. by the framework's own exception rendering) fatals.

Short description of the changes

  • Extract $request->method() access into a new httpMethod() helper that catches Throwable and falls back to 'unknown', used in both the pre and post hooks (also removes a redundant second call to method() that existed in the pre hook).
  • Add a httpFullUrl() helper guarding $request->fullUrl() the same way, since it independently calls getHost() and runs before httpHostName() in the attribute chain — so a hostile Host header could still crash the hook even with httpHostName() guarded.
  • Guard the one reachable getHost() call site inside httpHostName(). The getHost() fallback branch there is dead code (Illuminate\Http\Request always defines host()) and was already uncovered on main, so it's intentionally left unwrapped/untouched rather than dragging pre-existing untested lines into this diff.
  • Add regression tests: a malformed X-HTTP-METHOD-OVERRIDE header case and a malformed Host header case (the latter constructs the request manually, since MakesHttpRequests::call()'s Request::create() always overwrites HTTP_HOST from the parsed URI). Plus focused unit tests invoking httpMethod()/httpFullUrl()/httpHostName() directly via reflection so each guard's catch branch is exercised independent of call order.

Reverting the Kernel.php guards locally reproduces the exact warning and the dropped span for both the method-override and Host-header cases, confirming this is the same failure mode in both spots.

…verride

Request::method()/getMethod() throws SuspiciousOperationException for a
malformed method override (e.g. a `_method` or `X-HTTP-METHOD-OVERRIDE`
value that isn't plain letters, such as `_method=__construct` from a
scanner probe). The Kernel::handle pre-hook called this unguarded while
building the span, but this hook runs before the intercepted method body
executes and therefore before Laravel's own exception handling is
engaged. An uncaught throw here is reported by the extension as a raw
PHP warning printed outside of the normal response lifecycle, and the
span is silently dropped.

getHost() has the same failure mode for a Host header that fails
validation, reached via httpHostName().

Wrap both in try/catch and fall back to a safe placeholder so hostile
input can't break the instrumentation itself; the framework's own
request handling still processes (and can reject) the request normally
afterwards.
@welcome

welcome Bot commented Jul 7, 2026

Copy link
Copy Markdown

Thanks for opening your first pull request! If you haven't yet signed our Contributor License Agreement (CLA), then please do so that we can accept your contribution. A link should appear shortly in this PR if you have not already signed one.

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.07%. Comparing base (849e2ef) to head (c273cfc).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##               main     #650      +/-   ##
============================================
- Coverage     80.72%   80.07%   -0.65%     
- Complexity     1446     1600     +154     
============================================
  Files            95      118      +23     
  Lines          5343     6184     +841     
============================================
+ Hits           4313     4952     +639     
- Misses         1030     1232     +202     
Flag Coverage Δ
Context/Swoole 0.00% <ø> (ø)
Exporter/Instana 49.80% <ø> (ø)
Instrumentation/AwsSdk 82.14% <ø> (ø)
Instrumentation/CakePHP 20.42% <ø> (ø)
Instrumentation/CodeIgniter 79.31% <ø> (ø)
Instrumentation/Curl 86.88% <ø> (ø)
Instrumentation/Doctrine 92.82% <ø> (ø)
Instrumentation/ExtAmqp 88.80% <ø> (ø)
Instrumentation/Guzzle 76.25% <ø> (ø)
Instrumentation/HttpAsyncClient 78.94% <ø> (ø)
Instrumentation/HttpConfig 28.76% <ø> (ø)
Instrumentation/IO 0.00% <ø> (ø)
Instrumentation/Laravel 75.98% <100.00%> (?)
Instrumentation/Magento2 88.12% <ø> (ø)
Instrumentation/MongoDB 76.84% <ø> (ø)
Instrumentation/OpenAIPHP 86.71% <ø> (ø)
Instrumentation/PostgreSql 91.36% <ø> (ø)
Instrumentation/Psr14 77.41% <ø> (ø)
Instrumentation/Psr15 89.74% <ø> (ø)
Instrumentation/Psr16 97.43% <ø> (ø)
Instrumentation/Psr18 79.41% <ø> (ø)
Instrumentation/Psr6 97.56% <ø> (ø)
Instrumentation/ReactPHP 99.41% <ø> (ø)
Instrumentation/Session 94.28% <ø> (ø)
Instrumentation/Slim 84.21% <ø> (ø)
Propagation/CloudTrace 90.69% <ø> (ø)
Propagation/Instana 98.07% <ø> (ø)
Propagation/ServerTiming 94.73% <ø> (ø)
Propagation/TraceResponse 94.73% <ø> (ø)
ResourceDetectors/Azure 91.66% <ø> (ø)
ResourceDetectors/DigitalOcean 100.00% <ø> (ø)
Sampler/Xray 78.38% <ø> (ø)
Shims/OpenTracing 92.99% <ø> (ø)
SqlCommenter 95.58% <ø> (ø)
Utils/Test 87.79% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...vel/src/Hooks/Illuminate/Contracts/Http/Kernel.php 91.02% <100.00%> (ø)

... and 22 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 849e2ef...c273cfc. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hendrikheil
hendrikheil marked this pull request as ready for review July 7, 2026 08:15
@hendrikheil
hendrikheil requested a review from a team as a code owner July 7, 2026 08:15
fullUrl() (used for the URL_FULL attribute) internally calls getHost(),
which has the same pre-boot SuspiciousOperationException risk as
getMethod() for a malformed Host header, and it runs before
httpHostName() in the attribute chain — so a hostile Host header still
crashed the hook even with the previous fix. Guard it the same way as
httpMethod().

Add a regression test for the malformed-Host-header case (bypassing
call()'s URI-driven HTTP_HOST override to actually exercise it), plus
focused unit tests invoking the three guard methods directly so their
catch branches are exercised regardless of call order.

Also scope the httpHostName() try/catch to only the reachable branch:
Illuminate\Http\Request always defines host(), so the getHost()
fallback is dead code that was already uncovered on main; wrapping it
too just re-flagged pre-existing untested lines as part of this diff.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the Laravel Kernel::handle() instrumentation pre-hook against SuspiciousOperationException (and other throwables) triggered by malformed HTTP method override and Host headers, preventing uncaught pre-hook exceptions from breaking the response lifecycle and dropping spans.

Changes:

  • Add guarded helpers (httpMethod(), httpFullUrl()) to safely read request method and full URL in the pre-hook.
  • Wrap the host() call inside httpHostName() to avoid hook crashes on invalid Host headers.
  • Add unit and integration regression tests covering malformed method override and Host header cases.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/Instrumentation/Laravel/src/Hooks/Illuminate/Contracts/Http/Kernel.php Introduces guarded helpers and uses them in Kernel::handle pre/post hooks to prevent pre-hook throws from dropping spans.
src/Instrumentation/Laravel/tests/Integration/LaravelInstrumentationTest.php Adds integration coverage ensuring instrumentation remains functional with malformed method override / host headers.
src/Instrumentation/Laravel/tests/Unit/Hooks/Illuminate/Contracts/Http/KernelTest.php Adds focused unit tests for the new guard helpers’ exception-swallowing behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +167 to +171
$this->assertSame(200, $response->status());
$this->assertCount(1, $this->storage);
$span = $this->storage[0];
$this->assertSame('POST /', $span->getName());
}

@PuvaanRaaj PuvaanRaaj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the current Laravel hardening diff and the relevant CI failures. The guarded request accessors and regression coverage are directionally useful, but the branch still has a directly introduced quality-check failure.

$kernel = (new ReflectionClass(Kernel::class))->newInstanceWithoutConstructor();

$reflectionMethod = new ReflectionMethod(Kernel::class, $method);
$reflectionMethod->setAccessible(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This line currently fails the Laravel quality matrix with Psalm UnusedMethodCall. On the supported PHP 8.1+ runtimes, reflection members can be invoked without calling setAccessible(), so please remove this call (or otherwise adjust the helper) to restore the required quality checks.

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