From c47a6fb0557dc1cc692f74921476d83b68eb4d9b Mon Sep 17 00:00:00 2001 From: Diogo Martins Date: Fri, 13 Feb 2026 22:43:56 +0000 Subject: [PATCH] Fix kestrel - HEAD and OPTIONS headers allowed --- CHANGELOG.md | 2 ++ README.md | 1 + src/Http11Probe.Cli/Program.cs | 14 +++++++++++-- .../Reporting/ConsoleReporter.cs | 21 +++++++++++++++++++ src/Servers/AspNetMinimal/Program.cs | 8 +++++++ 5 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20c5a58..fdb76e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to Http11Probe are documented in this file. - **Row-click detail popup** — clicking a server row opens a modal showing that server's results for the current table in a vertical layout (Test, Expected, Got, Description) with section and table name in the header - **Truncation notice** — tooltip and modal now show a `[Truncated]` notice at the top when raw request/response data exceeds the 8,192-byte display limit - **Filter box** — text input above result tables to filter by server name, language, or test name; supports multiple comma-separated keywords +- **`--verbose` CLI flag** — prints the raw server response below each test result when enabled (`--verbose` or `-v`) ### Changed - **Horizontal column headers** — test name headers are now displayed horizontally instead of rotated at -55°, improving readability @@ -31,6 +32,7 @@ All notable changes to Http11Probe are documented in this file. - **Stronger sticky shadow on mobile** — increased shadow intensity for the pinned server name column - **Scrollable tooltips** — hover tooltips are now interactive and scrollable for large payloads (removed `pointer-events:none`, increased `max-height` to `60vh`) - **Larger click modal** — expanded from `max-width:700px` to `90vw` and `max-height` from `80vh` to `85vh` to better accommodate large request/response data +- **Kestrel HEAD/OPTIONS support** — added explicit HEAD and OPTIONS endpoint handlers to ASP.NET Minimal server so smuggling tests evaluate correctly instead of returning 405 - Raw request capture now includes truncation metadata when payload exceeds 8,192 bytes (`TestRunner.cs`) - Raw response capture now includes truncation metadata when response exceeds 8,192 bytes (`ResponseParser.cs`) diff --git a/README.md b/README.md index 8fc3b3e..1b2ea9f 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ dotnet run --project src/Http11Probe.Cli -- --host localhost --port 8080 | `--test` | Run only specific test IDs, case-insensitive (repeatable) | all | | `--timeout` | Connect and read timeout in seconds per test | `5` | | `--output` | Write JSON results to file | — | +| `--verbose`, `-v` | Print the raw server response for each test | off | ### Examples diff --git a/src/Http11Probe.Cli/Program.cs b/src/Http11Probe.Cli/Program.cs index 5a81962..f5d63fb 100644 --- a/src/Http11Probe.Cli/Program.cs +++ b/src/Http11Probe.Cli/Program.cs @@ -19,6 +19,9 @@ var outputOption = new Option("--output") { Description = "Write JSON results to this file path" }; +var verboseOption = new Option("--verbose", "-v") { Description = "Print the raw server response for each test" }; +verboseOption.DefaultValueFactory = _ => false; + var rootCommand = new RootCommand("Http11Probe — HTTP/1.1 server compliance & hardening tester") { hostOption, @@ -26,7 +29,8 @@ categoryOption, testOption, timeoutOption, - outputOption + outputOption, + verboseOption }; rootCommand.SetAction(async (parseResult, cancellationToken) => @@ -37,6 +41,7 @@ var timeout = parseResult.GetValue(timeoutOption); var testIds = parseResult.GetValue(testOption); var outputPath = parseResult.GetValue(outputOption); + var verbose = parseResult.GetValue(verboseOption); Console.WriteLine($" Http11Probe targeting {host}:{port}"); Console.WriteLine(); @@ -62,7 +67,12 @@ var runner = new TestRunner(options); ConsoleReporter.PrintHeader(); - var report = await runner.RunAsync(testCases, ConsoleReporter.PrintRow); + var report = await runner.RunAsync(testCases, result => + { + ConsoleReporter.PrintRow(result); + if (verbose) + ConsoleReporter.PrintRawResponse(result); + }); ConsoleReporter.PrintSummary(report); if (outputPath is not null) diff --git a/src/Http11Probe.Cli/Reporting/ConsoleReporter.cs b/src/Http11Probe.Cli/Reporting/ConsoleReporter.cs index 7a2b91e..55d8a98 100644 --- a/src/Http11Probe.Cli/Reporting/ConsoleReporter.cs +++ b/src/Http11Probe.Cli/Reporting/ConsoleReporter.cs @@ -62,6 +62,27 @@ public static void PrintRow(TestResult result) Console.WriteLine(); } + public static void PrintRawResponse(TestResult result) + { + if (result.Verdict == TestVerdict.Skip) + return; + + var raw = result.Response?.RawResponse; + if (raw is null) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine(" (no response)"); + Console.ResetColor(); + return; + } + + Console.ForegroundColor = ConsoleColor.DarkGray; + foreach (var line in raw.Split('\n')) + Console.WriteLine($" {line.TrimEnd('\r')}"); + Console.ResetColor(); + Console.WriteLine(); + } + public static void PrintSummary(TestRunReport report) { Console.WriteLine(" " + new string('─', 80)); diff --git a/src/Servers/AspNetMinimal/Program.cs b/src/Servers/AspNetMinimal/Program.cs index 3f48f6d..b0c5804 100644 --- a/src/Servers/AspNetMinimal/Program.cs +++ b/src/Servers/AspNetMinimal/Program.cs @@ -6,6 +6,14 @@ app.MapGet("/", () => "OK"); +app.MapMethods("/", ["HEAD"], () => Results.Ok()); + +app.MapMethods("/", ["OPTIONS"], (HttpContext ctx) => +{ + ctx.Response.Headers["Allow"] = "GET, HEAD, POST, OPTIONS"; + return Results.Ok(); +}); + app.MapPost("/", async (HttpContext ctx) => { using var reader = new StreamReader(ctx.Request.Body);