From bd7fd3a3ea537f632ac972ee3c76aa6a858d7053 Mon Sep 17 00:00:00 2001
From: Opathedane <88542122+Opathedane@users.noreply.github.com>
Date: Mon, 26 Oct 2026 17:59:38 +0100
Subject: [PATCH 1/2] fix: improve WampServer path inference and config
discovery
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Directory names like php7.4.33 were missed by bare X.Y.Z path inference, and
Wamp Apache/php.ini roots were incomplete — leaving ASERVER2-style hosts
under-assessed. Infer phpX.Y.Z as PATH_INFERRED/MEDIUM only, add bounded
Wamp conf/ini globs, and surface verified vs path-inferred vs unknown coverage
without claiming an active HTTP runtime.
Co-authored-by: Cursor
---
internal/audit/apache.go | 37 ++-
internal/audit/discovery_coverage.go | 67 +++-
internal/audit/host.go | 5 +-
internal/audit/path_version.go | 14 +-
internal/audit/php_ini.go | 1 +
internal/audit/report.go | 8 +-
internal/audit/report_view.go | 3 +
internal/audit/wamp_discovery_test.go | 313 ++++++++++++++++++
internal/audit/wellknown_stacks_test.go | 12 +-
.../windows_vm_report_regression_test.go | 4 +
internal/console/console.go | 4 +
testdata/sample-simplified-report.html | 2 +-
12 files changed, 451 insertions(+), 19 deletions(-)
create mode 100644 internal/audit/wamp_discovery_test.go
diff --git a/internal/audit/apache.go b/internal/audit/apache.go
index 3cf92fd..851d09a 100644
--- a/internal/audit/apache.go
+++ b/internal/audit/apache.go
@@ -30,6 +30,13 @@ var apacheConfigRoots = []string{
`C:\Program Files (x86)\Apache Software Foundation\Apache2.4\conf`,
}
+// Bounded single-segment globs for WampServer Apache layouts (expanded via filepath.Glob;
+// not recursive host-wide scanning). Presence of config does not prove an active runtime.
+var apacheConfigRootGlobs = []string{
+ `C:\wamp64\bin\apache\*\conf`,
+ `C:\wamp\bin\apache\*\conf`,
+}
+
var (
reApacheDirective = regexp.MustCompile(`(?i)^\s*(DocumentRoot|ServerName|ServerAlias|LoadModule|SetHandler|AddHandler|Action|ProxyPassMatch|ProxyPass|FcgidWrapper|Include|IncludeOptional|PHPIniDir)\s+(.+?)\s*$`)
reApacheVHostOpen = regexp.MustCompile(`(?i)^\s*]*)>`)
@@ -57,6 +64,18 @@ func scanApacheConfiguration(r *Report) {
func accessibleApacheRoots() []string {
var out []string
seen := map[string]bool{}
+ add := func(resolved string) {
+ fi, err := os.Lstat(resolved)
+ if err != nil || !fi.IsDir() || fi.Mode()&os.ModeSymlink != 0 {
+ return
+ }
+ key := normalizeInstallPath(resolved)
+ if seen[key] {
+ return
+ }
+ seen[key] = true
+ out = append(out, resolved)
+ }
for _, raw := range apacheConfigRoots {
resolved := raw
if runtime.GOOS != "windows" && isWindowsDrivePath(strings.ReplaceAll(raw, `\`, `/`)) {
@@ -65,16 +84,20 @@ func accessibleApacheRoots() []string {
if runtime.GOOS == "windows" {
resolved = filepath.Clean(os.ExpandEnv(raw))
}
- fi, err := os.Lstat(resolved)
- if err != nil || !fi.IsDir() || fi.Mode()&os.ModeSymlink != 0 {
+ add(resolved)
+ }
+ for _, pat := range apacheConfigRootGlobs {
+ if runtime.GOOS != "windows" && isWindowsDrivePath(strings.ReplaceAll(pat, `\`, `/`)) {
continue
}
- key := normalizeInstallPath(resolved)
- if seen[key] {
- continue
+ pattern := pat
+ if runtime.GOOS == "windows" {
+ pattern = filepath.Clean(os.ExpandEnv(pat))
+ }
+ matches, _ := filepath.Glob(pattern)
+ for _, m := range matches {
+ add(m)
}
- seen[key] = true
- out = append(out, resolved)
}
return out
}
diff --git a/internal/audit/discovery_coverage.go b/internal/audit/discovery_coverage.go
index 55ec635..1c86008 100644
--- a/internal/audit/discovery_coverage.go
+++ b/internal/audit/discovery_coverage.go
@@ -1,6 +1,10 @@
package audit
-import "strings"
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+)
// PHP discovery classification (internal). Used to keep HTML/CLI/CSV/summary semantics aligned
// without changing vulnerability-detection rules or scan-execution status.
@@ -182,6 +186,60 @@ func classifyPHPDiscovery(r *Report) string {
return phpDiscNoneIncomplete
}
+// BinaryInstallCoverage summarizes PHP binary candidate version provenance for operator-facing reports.
+// Path-inferred counts are not confirmed installations or proven vulnerability exposure.
+type BinaryInstallCoverage struct {
+ BinaryCandidates int
+ InstallDirs int
+ Verified int
+ PathInferred int
+ VersionUnknown int
+}
+
+// CountBinaryInstallCoverage tallies binary_path / binary_static_version / binary_path_inferred_version evidence.
+func CountBinaryInstallCoverage(r *Report) BinaryInstallCoverage {
+ var c BinaryInstallCoverage
+ if r == nil {
+ return c
+ }
+ dirs := map[string]struct{}{}
+ for _, e := range r.Evidence {
+ switch e.Type {
+ case "binary_static_version":
+ c.Verified++
+ c.BinaryCandidates++
+ if e.Location != "" {
+ dirs[normalizeInstallPath(filepath.Dir(e.Location))] = struct{}{}
+ }
+ case "binary_path_inferred_version":
+ c.PathInferred++
+ c.BinaryCandidates++
+ if e.Location != "" {
+ dirs[normalizeInstallPath(filepath.Dir(e.Location))] = struct{}{}
+ }
+ case "binary_path":
+ c.VersionUnknown++
+ c.BinaryCandidates++
+ if e.Location != "" {
+ dirs[normalizeInstallPath(filepath.Dir(e.Location))] = struct{}{}
+ }
+ }
+ }
+ c.InstallDirs = len(dirs)
+ return c
+}
+
+// FormatBinaryInstallCoverageDetail returns a concise operator-facing coverage sentence.
+func FormatBinaryInstallCoverageDetail(c BinaryInstallCoverage) string {
+ if c.BinaryCandidates == 0 {
+ return ""
+ }
+ return fmt.Sprintf(
+ "%d PHP binary candidate(s) across %d install director(ies): %d with verified binary metadata, %d path-inferred (MEDIUM confidence; not binary-verified), %d without comparable version. Path-inferred versions are not confirmed installations or proven vulnerability exposure and require manual verification. Not proof of the HTTP-serving runtime.",
+ c.BinaryCandidates, c.InstallDirs, c.Verified, c.PathInferred, c.VersionUnknown,
+ )
+}
+
// executiveVerificationGaps lists operator-facing gaps for the HTML "What could not be verified" card.
// Excludes Windows packages NOT_SUPPORTED and does not invent an HTTP-runtime gap when no PHP was found.
func executiveVerificationGaps(r *Report) []string {
@@ -211,6 +269,13 @@ func executiveVerificationGaps(r *Report) []string {
if rtLabel != "Verified" {
add("HTTP-serving PHP runtime")
}
+ cov := CountBinaryInstallCoverage(r)
+ if cov.PathInferred > 0 {
+ add("path-inferred PHP version(s) (not binary-verified; manual verification required)")
+ }
+ if cov.VersionUnknown > 0 {
+ add("PHP binary candidate(s) without comparable version")
+ }
for _, g := range relevantPHPInventoryGaps(r) {
add(g)
}
diff --git a/internal/audit/host.go b/internal/audit/host.go
index f518bf6..f1979b4 100644
--- a/internal/audit/host.go
+++ b/internal/audit/host.go
@@ -235,7 +235,7 @@ func scanBinaries(r *Report, opts HostScanOptions) []deepDiscoverOutcome {
r.AddCheck("deep_discovery", status, detail)
}
- versioned, inferred, partialInspect := 0, 0, 0
+ versioned, inferred, unknownVer, partialInspect := 0, 0, 0, 0
distinctDirs := map[string]struct{}{}
for _, e := range r.Evidence {
switch e.Type {
@@ -249,6 +249,7 @@ func scanBinaries(r *Report, opts HostScanOptions) []deepDiscoverOutcome {
inferred++
distinctDirs[normalizeInstallPath(filepath.Dir(e.Location))] = struct{}{}
case "binary_path":
+ unknownVer++
distinctDirs[normalizeInstallPath(filepath.Dir(e.Location))] = struct{}{}
}
}
@@ -256,7 +257,7 @@ func scanBinaries(r *Report, opts HostScanOptions) []deepDiscoverOutcome {
if deepTruncated {
status = "TRUNCATED"
}
- detail := fmt.Sprintf("Found %d PHP-like binary paths across %d install director(ies) (%d with verified static version markers, %d path-inferred); no binaries executed", found, len(distinctDirs), versioned, inferred)
+ detail := fmt.Sprintf("Found %d PHP-like binary paths across %d install director(ies) (%d with verified static version markers, %d path-inferred, %d without comparable version); path-inferred versions are not binary-verified and are not proof of the HTTP-serving runtime; no binaries executed", found, len(distinctDirs), versioned, inferred, unknownVer)
if runtime.GOOS == "windows" {
detail += `; searched well-known dirs, %LOCALAPPDATA%\Programs\PHP (bounded junction resolve), all eligible PATH entries (not only the first match), IIS/Apache handler follow-up when present, operator --config-root, and optional --deep-root`
} else {
diff --git a/internal/audit/path_version.go b/internal/audit/path_version.go
index ef26f64..9e1f4fb 100644
--- a/internal/audit/path_version.go
+++ b/internal/audit/path_version.go
@@ -6,11 +6,15 @@ import (
"strings"
)
-// Exact PHP patch versions embedded as directory segments (e.g. ...\PHP\8.4.24\nts\x64\).
-var pathPHPVersionSegment = regexp.MustCompile(`(?i)^(\d+)\.(\d+)\.(\d+)$`)
+// Exact PHP patch versions as directory segments: bare X.Y.Z (e.g. ...\PHP\8.4.24\nts\x64\)
+// or WampServer-style phpX.Y.Z (e.g. ...\wamp64\bin\php\php7.4.33\php.exe).
+// Capture group 1 is always the numeric x.y.z (never the "php" prefix).
+var pathPHPVersionSegment = regexp.MustCompile(`(?i)^(?:php)?(\d+\.\d+\.\d+)$`)
// VersionFromInstallPath extracts a PHP x.y.z version from well-known installer path
// segments. Returns empty when the path only uses aliases such as "current".
+// Inferred versions keep PATH_INFERRED / MEDIUM provenance via VersionFromBinary;
+// they must not be treated as verified binary metadata or as the active HTTP runtime.
func VersionFromInstallPath(path string) (version string, source string) {
cleaned := filepath.Clean(path)
parts := strings.FieldsFunc(cleaned, func(r rune) bool {
@@ -18,15 +22,15 @@ func VersionFromInstallPath(path string) (version string, source string) {
})
var found []string
for _, p := range parts {
- if pathPHPVersionSegment.MatchString(p) {
- found = append(found, p)
+ if m := pathPHPVersionSegment.FindStringSubmatch(p); len(m) == 2 {
+ found = append(found, m[1])
}
}
if len(found) == 0 {
return "", ""
}
// Prefer the last (deepest) exact version segment; official Windows layout uses
- // Programs\PHP\\nts|ts\x64|x86\php.exe.
+ // Programs\PHP\\nts|ts\x64|x86\php.exe. WampServer uses bin\php\phpX.Y.Z\.
return found[len(found)-1], "install_path_version_segment"
}
diff --git a/internal/audit/php_ini.go b/internal/audit/php_ini.go
index cb95afd..f690ed4 100644
--- a/internal/audit/php_ini.go
+++ b/internal/audit/php_ini.go
@@ -164,6 +164,7 @@ func phpIniWellKnownPatterns() []string {
`C:\php\php.ini-development`,
`C:\xampp\php\php.ini`,
`C:\wamp\bin\php\*\php.ini`,
+ `C:\wamp64\bin\php\*\php.ini`,
`C:\laragon\bin\php\*\php.ini`,
`C:\inetpub\PHP\*\php.ini`,
`C:\Program Files\PHP\*\php.ini`,
diff --git a/internal/audit/report.go b/internal/audit/report.go
index bbf3b18..74a9a41 100644
--- a/internal/audit/report.go
+++ b/internal/audit/report.go
@@ -158,11 +158,17 @@ details{border:1px solid var(--line);border-radius:8px;background:var(--card);ma
b.WriteString("Only filenames or configuration hints were seen; treat as unconfirmed.")
case phpDiscVersionUnknown:
b.WriteString("PHP-related evidence was observed, but no comparable release version was extracted.")
+ if covDetail := FormatBinaryInstallCoverageDetail(CountBinaryInstallCoverage(r)); covDetail != "" {
+ b.WriteString(" " + h(covDetail))
+ }
default:
if len(versions) == 0 {
b.WriteString("No comparable PHP release versions were extracted from this scan scope.")
} else {
- b.WriteString("Observed inventory versions (not proof of the HTTP-serving runtime): " + h(strings.Join(versions, ", ")))
+ b.WriteString("Observed inventory versions (not proof of the HTTP-serving runtime): " + h(strings.Join(versions, ", ")) + ".")
+ }
+ if covDetail := FormatBinaryInstallCoverageDetail(CountBinaryInstallCoverage(r)); covDetail != "" {
+ b.WriteString(" " + h(covDetail))
}
}
b.WriteString(`
`)
diff --git a/internal/audit/report_view.go b/internal/audit/report_view.go
index d102e33..3964f52 100644
--- a/internal/audit/report_view.go
+++ b/internal/audit/report_view.go
@@ -653,6 +653,9 @@ func BuildFocusedNextSteps(r *Report) []string {
if kind == phpDiscVersionUnknown {
add("PHP-related evidence was observed without a comparable patch version. Inventory the exact active PHP patch versions for production web handlers before assigning vulnerability status.")
}
+ if cov := CountBinaryInstallCoverage(r); cov.PathInferred > 0 {
+ add("Manually verify path-inferred PHP version(s) (directory-derived only; not binary metadata). Treat them as inventory leads requiring confirmation — not as confirmed installations or proven vulnerability exposure.")
+ }
rtLabel, _ := humanRuntime(r)
if rtLabel != "Verified" && rtLabel != "Not applicable" {
add("Identify which PHP binary or SAPI actually serves HTTP (FPM pool, Apache module, IIS handler, container entrypoint). Package and CLI versions alone are not sufficient.")
diff --git a/internal/audit/wamp_discovery_test.go b/internal/audit/wamp_discovery_test.go
new file mode 100644
index 0000000..01c7f84
--- /dev/null
+++ b/internal/audit/wamp_discovery_test.go
@@ -0,0 +1,313 @@
+package audit
+
+import (
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestVersionFromInstallPathWampAndBare(t *testing.T) {
+ cases := []struct {
+ path string
+ want string
+ }{
+ {`C:\wamp64\bin\php\php7.4.33\php.exe`, "7.4.33"},
+ {`C:\wamp\bin\php\php8.2.12\php-cgi.exe`, "8.2.12"},
+ {`C:\Users\x\AppData\Local\Programs\PHP\8.4.24\nts\x64\php.exe`, "8.4.24"},
+ {`/opt/php/8.3.36/bin/php`, "8.3.36"},
+ {`C:\wamp64\bin\php\php7.4\php.exe`, ""}, // no patch segment
+ {`C:\wamp64\bin\php\php74\php.exe`, ""}, // ambiguous
+ {`C:\wamp64\bin\php\current\php.exe`, ""}, // alias only
+ {`C:\wamp64\bin\php\php-7.4.33\php.exe`, ""}, // hyphenated prefix
+ {`C:\wamp64\bin\php\something7.4.33\php.exe`, ""}, // not a version segment
+ {`C:\wamp64\bin\php\7.4.33rc1\php.exe`, ""}, // prerelease token
+ }
+ for _, tc := range cases {
+ v, src := VersionFromInstallPath(tc.path)
+ if v != tc.want {
+ t.Fatalf("path %q: got version %q want %q", tc.path, v, tc.want)
+ }
+ if tc.want != "" && src != "install_path_version_segment" {
+ t.Fatalf("path %q: unexpected source %q", tc.path, src)
+ }
+ if tc.want == "" && src != "" {
+ t.Fatalf("path %q: expected empty source, got %q", tc.path, src)
+ }
+ }
+}
+
+func TestWampMarkerFreeBinaryKeepsPathInferredMedium(t *testing.T) {
+ root := t.TempDir()
+ binDir := filepath.Join(root, "wamp64", "bin", "php", "php7.4.33")
+ if err := os.MkdirAll(binDir, 0755); err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(binDir, "php.exe")
+ if err := os.WriteFile(path, []byte("MZ\x00\x00 marker-free WampServer fixture"), 0644); err != nil {
+ t.Fatal(err)
+ }
+ res := VersionFromBinary(path)
+ if res.Version != "7.4.33" || res.Provenance != "path_inferred" || res.Confidence != "MEDIUM" {
+ t.Fatalf("%+v", res)
+ }
+ r := NewReport("ASERVER2-fixture", "windows", "amd64")
+ AddBinaryVersionEvidence(r, path, "php.exe", "Windows well-known WampServer static binary inspection")
+ if len(r.Evidence) != 1 || r.Evidence[0].Type != "binary_path_inferred_version" {
+ t.Fatalf("%+v", r.Evidence)
+ }
+ if r.Evidence[0].Interpretation != "PATH_INFERRED_VERSION_NOT_BINARY_VERIFIED_NOT_RUNTIME_ATTESTATION" {
+ t.Fatal(r.Evidence[0].Interpretation)
+ }
+ if !strings.Contains(r.Evidence[0].Detail, "provenance=path_inferred") || !strings.Contains(r.Evidence[0].Detail, "confidence=MEDIUM") {
+ t.Fatal(r.Evidence[0].Detail)
+ }
+}
+
+func TestWampApacheAndPHPIniWellKnownPatterns(t *testing.T) {
+ joinedApache := strings.Join(apacheConfigRootGlobs, "\n")
+ for _, need := range []string{
+ `C:\wamp64\bin\apache\*\conf`,
+ `C:\wamp\bin\apache\*\conf`,
+ } {
+ if !strings.Contains(joinedApache, need) {
+ t.Fatalf("missing apache glob %q in %v", need, apacheConfigRootGlobs)
+ }
+ }
+ if runtime.GOOS == "windows" {
+ iniJoined := strings.Join(phpIniWellKnownPatterns(), "\n")
+ if !strings.Contains(iniJoined, `C:\wamp64\bin\php\*\php.ini`) {
+ t.Fatalf("missing wamp64 php.ini pattern in %v", phpIniWellKnownPatterns())
+ }
+ } else {
+ // Patterns are Windows-only; assert the source list still documents wamp64.
+ src, err := os.ReadFile("php_ini.go")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(src), `C:\wamp64\bin\php\*\php.ini`) {
+ t.Fatal("php_ini.go must list wamp64 php.ini well-known pattern")
+ }
+ }
+}
+
+func TestWampApacheGlobExpansionDiscoversConf(t *testing.T) {
+ root := t.TempDir()
+ confDir := filepath.Join(root, "wamp64", "bin", "apache", "apache2.4.54.2", "conf")
+ if err := os.MkdirAll(confDir, 0755); err != nil {
+ t.Fatal(err)
+ }
+ httpd := filepath.Join(confDir, "httpd.conf")
+ body := `ServerRoot "` + filepath.ToSlash(filepath.Join(root, "wamp64", "bin", "apache", "apache2.4.54.2")) + `"
+Listen 80
+LoadModule php7_module "` + filepath.ToSlash(filepath.Join(root, "wamp64", "bin", "php", "php7.4.33", "php7apache2_4.dll")) + `"
+
+ ServerName aserver2.local
+ DocumentRoot "` + filepath.ToSlash(filepath.Join(root, "www")) + `"
+
+`
+ if err := os.WriteFile(httpd, []byte(body), 0644); err != nil {
+ t.Fatal(err)
+ }
+ matches, err := filepath.Glob(filepath.Join(root, "wamp64", "bin", "apache", "*", "conf"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(matches) != 1 {
+ t.Fatalf("expected 1 conf root via bounded glob, got %v", matches)
+ }
+ r := NewReport("wamp-apache", "windows", "amd64")
+ ParseApacheConfigForTest(r, confDir)
+ sawMod, sawVHost := false, false
+ for _, e := range r.Evidence {
+ switch e.Type {
+ case "apache_php_module_config":
+ sawMod = true
+ if e.Interpretation == "" || strings.Contains(strings.ToLower(e.Interpretation), "active") && strings.Contains(strings.ToLower(e.Interpretation), "verified") {
+ // Must not claim verified active runtime.
+ }
+ if strings.Contains(e.Interpretation, "ACTIVE_RUNTIME") && !strings.Contains(e.Interpretation, "NOT") {
+ t.Fatalf("must not attribute active runtime from static config: %s", e.Interpretation)
+ }
+ case "apache_vhost_config":
+ sawVHost = true
+ if e.Interpretation != "CONFIGURED_VHOST_PATH_NOT_PROOF_PHP_IS_SERVED_OR_ACTIVE_RUNTIME" {
+ t.Fatal(e.Interpretation)
+ }
+ }
+ }
+ if !sawMod || !sawVHost {
+ t.Fatalf("mod=%v vhost=%v evidence=%+v", sawMod, sawVHost, r.Evidence)
+ }
+ rt, _ := humanRuntime(r)
+ if rt == "Verified" {
+ t.Fatal("Apache config alone must not verify active web runtime")
+ }
+}
+
+func TestASERVER2WampStyleFixtureE2E(t *testing.T) {
+ // Synthetic ASERVER2-style WampServer layout: phpX.Y.Z dirs, marker-free binaries,
+ // Apache conf + php.ini. No PHP execution; no false active-runtime attribution.
+ root := t.TempDir()
+ phpDir := filepath.Join(root, "wamp64", "bin", "php", "php7.4.33")
+ confDir := filepath.Join(root, "wamp64", "bin", "apache", "apache2.4.54.2", "conf")
+ www := filepath.Join(root, "www")
+ for _, d := range []string{phpDir, confDir, www} {
+ if err := os.MkdirAll(d, 0755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ phpBin := filepath.Join(phpDir, "php.exe")
+ phpCGI := filepath.Join(phpDir, "php-cgi.exe")
+ phpIni := filepath.Join(phpDir, "php.ini")
+ markerFree := []byte("MZ\x00\x00 ASERVER2 WampServer marker-free fixture")
+ if err := os.WriteFile(phpBin, markerFree, 0644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(phpCGI, markerFree, 0644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(phpIni, []byte("memory_limit = 128M\nexpose_php = Off\n"), 0644); err != nil {
+ t.Fatal(err)
+ }
+ httpd := filepath.Join(confDir, "httpd.conf")
+ if err := os.WriteFile(httpd, []byte(
+ `LoadModule php7_module "`+filepath.ToSlash(filepath.Join(phpDir, "php7apache2_4.dll"))+`"
+PHPIniDir "`+filepath.ToSlash(phpDir)+`"
+
+ ServerName aserver2.example.local
+ DocumentRoot "`+filepath.ToSlash(www)+`"
+ Action application/x-httpd-php "`+filepath.ToSlash(phpCGI)+`"
+ AddHandler application/x-httpd-php .php
+
+`), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ r := NewReport("ASERVER2", "windows", "amd64")
+ r.GeneratedUTC = time.Date(2026, 10, 26, 12, 0, 0, 0, time.UTC)
+ src := "Windows well-known WampServer static binary inspection"
+ AddBinaryVersionEvidence(r, phpBin, "php.exe", src)
+ AddBinaryVersionEvidence(r, phpCGI, "php-cgi.exe", src)
+ ParseApacheConfigForTest(r, confDir)
+
+ oldPatterns := phpIniWellKnownPatternsFn
+ phpIniWellKnownPatternsFn = func() []string {
+ return []string{filepath.Join(root, "wamp64", "bin", "php", "*", "php.ini")}
+ }
+ defer func() { phpIniWellKnownPatternsFn = oldPatterns }()
+ scanConfigs(r, HostScanOptions{}, nil)
+
+ EvaluatePatchPosture(r)
+ FinalizeSummary(r)
+
+ inferred, verified, unknown := 0, 0, 0
+ for _, e := range r.Evidence {
+ switch e.Type {
+ case "binary_path_inferred_version":
+ inferred++
+ if e.Version != "7.4.33" {
+ t.Fatalf("inferred version=%q", e.Version)
+ }
+ case "binary_static_version":
+ verified++
+ case "binary_path":
+ unknown++
+ }
+ }
+ // Well-known discovery yields php.exe + php-cgi.exe; Apache handler follow-up may
+ // re-inspect php-cgi.exe — all remain path_inferred, never verified metadata.
+ if inferred < 2 || verified != 0 || unknown != 0 {
+ t.Fatalf("inferred=%d verified=%d unknown=%d evidence=%+v", inferred, verified, unknown, r.Evidence)
+ }
+
+ cov := CountBinaryInstallCoverage(r)
+ if cov.PathInferred < 2 || cov.Verified != 0 || cov.VersionUnknown != 0 || cov.BinaryCandidates < 2 {
+ t.Fatalf("%+v", cov)
+ }
+
+ if r.VulnerabilityStatus != "LEGACY_PHP_VENDOR_PATCH_VERIFICATION_REQUIRED" {
+ t.Fatalf("status=%s assessments=%+v", r.VulnerabilityStatus, r.PatchAssessments)
+ }
+ for _, a := range r.PatchAssessments {
+ if a.EvidenceType != "binary_path_inferred_version" || a.Confidence != "MEDIUM" {
+ t.Fatalf("%+v", a)
+ }
+ if a.Status != "LEGACY_VENDOR_VERIFICATION_REQUIRED" {
+ t.Fatalf("%+v", a)
+ }
+ if !strings.Contains(a.Qualification, "PATH-INFERRED") {
+ t.Fatal(a.Qualification)
+ }
+ }
+
+ rt, _ := humanRuntime(r)
+ if rt == "Verified" {
+ t.Fatal("must not claim verified active HTTP runtime from Wamp inventory/config")
+ }
+
+ gaps := executiveVerificationGaps(r)
+ joinedGaps := strings.Join(gaps, "; ")
+ if !strings.Contains(joinedGaps, "path-inferred") || !strings.Contains(joinedGaps, "HTTP-serving PHP runtime") {
+ t.Fatalf("gaps=%v", gaps)
+ }
+
+ steps := BuildFocusedNextSteps(r)
+ stepText := strings.Join(steps, "\n")
+ if !strings.Contains(strings.ToLower(stepText), "legacy") {
+ t.Fatalf("expected legacy review messaging in next steps: %v", steps)
+ }
+ if !strings.Contains(stepText, "path-inferred") {
+ t.Fatalf("expected path-inferred manual verification step: %v", steps)
+ }
+
+ html := string(EncodeHTML(r))
+ for _, need := range []string{
+ "7.4.33",
+ "path-inferred",
+ "verified binary metadata",
+ "manual verification",
+ "not a clearance",
+ } {
+ if !strings.Contains(strings.ToLower(html), strings.ToLower(need)) {
+ t.Fatalf("HTML missing %q", need)
+ }
+ }
+ rtLabel, _ := humanRuntime(r)
+ if rtLabel == "Verified" || strings.Contains(html, `Verified
`) {
+ t.Fatal("HTML/CLI must not show Verified active web runtime")
+ }
+
+ csvBytes, err := EncodeCSV(r)
+ if err != nil {
+ t.Fatal(err)
+ }
+ csv := string(csvBytes)
+ if !strings.Contains(csv, "binary_path_inferred_version") || !strings.Contains(csv, "7.4.33") {
+ t.Fatalf("CSV missing inferred evidence: %s", csv[:min(500, len(csv))])
+ }
+ if !strings.Contains(csv, "confidence=MEDIUM") || !strings.Contains(csv, "provenance=path_inferred") {
+ t.Fatal("CSV must retain path_inferred MEDIUM provenance detail")
+ }
+
+ fleetBytes, err := EncodeFleetCSV(r)
+ if err != nil {
+ t.Fatal(err)
+ }
+ fleet := string(fleetBytes)
+ if !strings.Contains(fleet, "path_inferred") || !strings.Contains(fleet, "MEDIUM") {
+ t.Fatalf("fleet CSV missing path_inferred/MEDIUM")
+ }
+
+ // Bare X.Y.Z layout still works alongside phpX.Y.Z.
+ bareDir := filepath.Join(root, "wamp64", "bin", "php", "8.2.28")
+ _ = os.MkdirAll(bareDir, 0755)
+ bareBin := filepath.Join(bareDir, "php.exe")
+ _ = os.WriteFile(bareBin, []byte("MZ\x00 bare-version-dir marker-free"), 0644)
+ v, _ := VersionFromInstallPath(bareBin)
+ if v != "8.2.28" {
+ t.Fatalf("bare layout regression: %q", v)
+ }
+}
diff --git a/internal/audit/wellknown_stacks_test.go b/internal/audit/wellknown_stacks_test.go
index 97eb046..9283b15 100644
--- a/internal/audit/wellknown_stacks_test.go
+++ b/internal/audit/wellknown_stacks_test.go
@@ -42,9 +42,11 @@ func TestLinuxWellKnownGlobsIncludeRemiCpanelLocalOpt(t *testing.T) {
func TestWindowsStackFixtureDiscoveryViaGlob(t *testing.T) {
// Simulate WAMP/Laragon/tools layouts under a temp drive-like root using Glob semantics.
+ // Includes both bare X.Y.Z and WampServer phpX.Y.Z directory names.
root := t.TempDir()
layouts := []string{
filepath.Join(root, "wamp", "bin", "php", "8.2.28", "php.exe"),
+ filepath.Join(root, "wamp64", "bin", "php", "php7.4.33", "php.exe"),
filepath.Join(root, "wamp64", "bin", "php", "8.3.36", "php-cgi.exe"),
filepath.Join(root, "laragon", "bin", "php", "8.4.24", "php.exe"),
filepath.Join(root, "tools", "php", "8.4.24", "php.exe"),
@@ -55,6 +57,7 @@ func TestWindowsStackFixtureDiscoveryViaGlob(t *testing.T) {
}
patterns := []string{
filepath.Join(root, "wamp", "bin", "php", "*", "php.exe"),
+ filepath.Join(root, "wamp64", "bin", "php", "*", "php.exe"),
filepath.Join(root, "wamp64", "bin", "php", "*", "php-cgi.exe"),
filepath.Join(root, "laragon", "bin", "php", "*", "php.exe"),
filepath.Join(root, "tools", "php", "*", "php.exe"),
@@ -69,8 +72,13 @@ func TestWindowsStackFixtureDiscoveryViaGlob(t *testing.T) {
found[normalizeInstallPath(m)] = true
}
}
- if len(found) != 4 {
- t.Fatalf("expected 4 stack binaries via bounded globs, got %d (%v)", len(found), found)
+ if len(found) != 5 {
+ t.Fatalf("expected 5 stack binaries via bounded globs, got %d (%v)", len(found), found)
+ }
+ wampPHP := filepath.Join(root, "wamp64", "bin", "php", "php7.4.33", "php.exe")
+ v, _ := VersionFromInstallPath(wampPHP)
+ if v != "7.4.33" {
+ t.Fatalf("phpX.Y.Z glob hit must still path-infer: %q", v)
}
}
diff --git a/internal/audit/windows_vm_report_regression_test.go b/internal/audit/windows_vm_report_regression_test.go
index 5fc0d7b..3f18e17 100644
--- a/internal/audit/windows_vm_report_regression_test.go
+++ b/internal/audit/windows_vm_report_regression_test.go
@@ -208,4 +208,8 @@ func TestVersionFromInstallPathSegments(t *testing.T) {
if v != "" {
t.Fatalf("current alone must not invent a version: %q", v)
}
+ v, src = VersionFromInstallPath(`C:\wamp64\bin\php\php7.4.33\php.exe`)
+ if v != "7.4.33" || src != "install_path_version_segment" {
+ t.Fatalf("wamp phpX.Y.Z: %q %q", v, src)
+ }
}
diff --git a/internal/console/console.go b/internal/console/console.go
index 337e993..5b56630 100644
--- a/internal/console/console.go
+++ b/internal/console/console.go
@@ -172,6 +172,10 @@ func (p *Printer) Summary(r *audit.Report, htmlPath, csvPath string, elapsed tim
} else {
fmt.Fprintln(p.out, " Observed versions: none in this scan scope (absence is not proof PHP is missing)")
}
+ if cov := audit.CountBinaryInstallCoverage(r); cov.BinaryCandidates > 0 {
+ fmt.Fprintf(p.out, " Binary coverage: %d candidate(s); %d verified metadata, %d path-inferred, %d without comparable version (path-inferred ≠ confirmed install / active runtime)\n",
+ cov.BinaryCandidates, cov.Verified, cov.PathInferred, cov.VersionUnknown)
+ }
groups := audit.GroupInstallations(r)
if len(groups) > 0 {
diff --git a/testdata/sample-simplified-report.html b/testdata/sample-simplified-report.html
index ee34cbf..5815aea 100644
--- a/testdata/sample-simplified-report.html
+++ b/testdata/sample-simplified-report.html
@@ -16,4 +16,4 @@
.footer{margin-top:28px;color:var(--muted);font-size:.85rem}
@media print{body{background:#fff;padding:0}details{break-inside:avoid}details:not([open]){border-style:dashed}a{color:inherit}}
@media (max-width:600px){body{padding:16px 12px}.cards{gap:10px}.card{min-height:0;padding:14px}}
-This report is not a clearance. It assesses observed PHP inventory against upstream advisories. It does not prove the HTTP-serving runtime is patched or that the host is uncompromised. Missing findings are not proof of safety.
Patch assessment
Update review required
Primary outcome for CVE-2026-109618 / CVE-2026-109619. Upstream thresholds (2026-10-21): 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 (upstream; vendor backports require separate verification). Rules reviewed 2026-10-24.
PHP installations found
Yes
Observed inventory versions (not proof of the HTTP-serving runtime): 8.3.24, 8.4.11
Active web runtime
Candidates identified
Installed packages, binaries, or processes were observed; the HTTP-serving PHP runtime is not verified.
What could not be verified
HTTP-serving PHP runtime
Incomplete evidence does not prove PHP is missing from the host, and does not prove the host is patched.
Recommended next steps
- Update or verify vendor backports for: PHP 8.2.34 (update toward 8.2.35 or verify an equivalent vendor security backport); PHP 8.3.24 @ /usr/sbin/php-fpm8.3 (update toward 8.3.36 or verify an equivalent vendor security backport); PHP 8.3.24 (update toward 8.3.36 or verify an equivalent vendor security backport); PHP 8.3.24 @ /opt/legacy-app/sbin/php-fpm (update toward 8.3.36 or verify an equivalent vendor security backport); PHP 8.4.11 (update toward 8.4.27 or verify an equivalent vendor security backport). Restart PHP-FPM, Apache/mod_php, IIS FastCGI, or other workers that may load these builds, then confirm the serving runtime version.
- Identify which PHP binary or SAPI actually serves HTTP (FPM pool, Apache module, IIS handler, container entrypoint). Package and CLI versions alone are not sufficient.
- Compromise investigation was not requested. Use --access-log / --error-log / --process-telemetry / --webroot for evidence-based review; missing inputs leave statuses NOT_ASSESSED, not negative.
- Validate remediation by checking the active web handler version after restart, and review official guidance at https://www.php.net/archive/2026.php#2026-10-21-1.
PHP installations (grouped)
| PHP version | Minimum fixed | Assessment | Lifecycle (informational) |
|---|
| PHP 8.2.34 | 8.2.35 | Update review required | PHP 8.2: security-only until 31 Dec 2026 (php.net) — plan upgrade |
| PHP 8.3.24 @ /usr/sbin/php-fpm8.3 | 8.3.36 | Update review required | PHP 8.3: security-only until 31 Dec 2027 (php.net) |
| PHP 8.3.24 | 8.3.36 | Update review required | PHP 8.3: security-only until 31 Dec 2027 (php.net) |
| PHP 8.3.24 @ /opt/legacy-app/sbin/php-fpm | 8.3.36 | Update review required | PHP 8.3: security-only until 31 Dec 2027 (php.net) |
| PHP 8.4.11 | 8.4.27 | Update review required | PHP 8.4: active support until 31 Dec 2026; security until 31 Dec 2028 (php.net) |
PHP 8.2.34 — packages, binaries and evidence
Assessment: Update review required · Priority: CRITICAL
| Component | Type | Version | Action / limitations |
|---|
| Dockerfile | repository_declaration | 8.2.34 | Update the PHP web-serving runtime to 8.2.35 or later on this supported branch (or verify an equivalent vendor security backport), restart every PHP worker and verify the serving runtime. confidence=MEDIUM; Source declaration only; may not match production. Version below the upstream fix; vendor backports and actual deployment remain unverified. |
Related evidence (includes extension packages kept for completeness):
| Name | Version | Type | Source |
|---|
| Dockerfile | 8.2.34 | repository_declaration | dockerfile |
PHP 8.3.24 @ /usr/sbin/php-fpm8.3 — packages, binaries and evidence
Assessment: Update review required · Priority: CRITICAL
| Component | Type | Version | Action / limitations |
|---|
| php8.3-fpm | installed_package | 8.3.24 | Update the PHP web-serving runtime to 8.3.36 or later on this supported branch (or verify an equivalent vendor security backport), restart every PHP worker and verify the serving runtime. confidence=MEDIUM; Static inventory only; not attestation of the serving HTTP runtime. A vendor may have backported a fix without changing the upstream version string. Version below the upstream fix; vendor backports and actual deployment remain unverified. |
| php-fpm8.3 | binary_static_version | 8.3.24 | Update the PHP web-serving runtime to 8.3.36 or later on this supported branch (or verify an equivalent vendor security backport), restart every PHP worker and verify the serving runtime. confidence=HIGH; Verified from passive binary metadata (embedded string and/or PE resource); not proof the binary serves HTTP or is the active runtime. Version below the upstream fix; vendor backports and actual deployment remain unverified. |
Related evidence (includes extension packages kept for completeness):
| Name | Version | Type | Source |
|---|
| php-fpm8.3 | 8.3.24 | binary_static_version | well-known path static binary inspection |
PHP 8.3.24 — packages, binaries and evidence
Assessment: Update review required · Priority: CRITICAL
| Component | Type | Version | Action / limitations |
|---|
| php8.3-cli | installed_package | 8.3.24 | Update the PHP web-serving runtime to 8.3.36 or later on this supported branch (or verify an equivalent vendor security backport), restart every PHP worker and verify the serving runtime. confidence=MEDIUM; Static inventory only; not attestation of the serving HTTP runtime. A vendor may have backported a fix without changing the upstream version string. Version below the upstream fix; vendor backports and actual deployment remain unverified. |
| php8.3-common | installed_package | 8.3.24 | Update the PHP web-serving runtime to 8.3.36 or later on this supported branch (or verify an equivalent vendor security backport), restart every PHP worker and verify the serving runtime. confidence=MEDIUM; Static inventory only; not attestation of the serving HTTP runtime. A vendor may have backported a fix without changing the upstream version string. Version below the upstream fix; vendor backports and actual deployment remain unverified. |
Related evidence (includes extension packages kept for completeness):
| Name | Version | Type | Source |
|---|
| php8.3-fpm | 8.3.24 | installed_package | dpkg installed package metadata |
| php8.3-cli | 8.3.24 | installed_package | dpkg installed package metadata |
| php8.3-common | 8.3.24 | installed_package | dpkg installed package metadata |
| php8.3-mysql | 8.3.24 | installed_package | dpkg installed package metadata |
PHP 8.3.24 @ /opt/legacy-app/sbin/php-fpm — packages, binaries and evidence
Assessment: Update review required · Priority: CRITICAL
| Component | Type | Version | Action / limitations |
|---|
| php-fpm | binary_static_version | 8.3.24 | Update the PHP web-serving runtime to 8.3.36 or later on this supported branch (or verify an equivalent vendor security backport), restart every PHP worker and verify the serving runtime. confidence=HIGH; Verified from passive binary metadata (embedded string and/or PE resource); not proof the binary serves HTTP or is the active runtime. Version below the upstream fix; vendor backports and actual deployment remain unverified. |
Related evidence (includes extension packages kept for completeness):
| Name | Version | Type | Source |
|---|
| php-fpm | 8.3.24 | binary_static_version | well-known path static binary inspection |
PHP 8.4.11 — packages, binaries and evidence
Assessment: Update review required · Priority: CRITICAL
| Component | Type | Version | Action / limitations |
|---|
| php8.4-fpm | installed_package | 8.4.11 | Update the PHP web-serving runtime to 8.4.27 or later on this supported branch (or verify an equivalent vendor security backport), restart every PHP worker and verify the serving runtime. confidence=MEDIUM; Static inventory only; not attestation of the serving HTTP runtime. A vendor may have backported a fix without changing the upstream version string. Version below the upstream fix; vendor backports and actual deployment remain unverified. |
Related evidence (includes extension packages kept for completeness):
| Name | Version | Type | Source |
|---|
| php8.4-fpm | 8.4.11 | installed_package | dpkg installed package metadata |
Update sources: OS/LTS vendor security packages; PHP for Windows; official source downloads. Advisory: https://www.php.net/archive/2026.php#2026-10-21-1.
Additional security advisories
CVE-2026-115560 is reviewed separately from CVE-2026-109618 / CVE-2026-109619. Conclusions use observed configuration text only — never a definitive vulnerable or safe verdict. No fixed-version threshold is published yet.
| CVE | Status | Observed configuration | Missing evidence | Recommended action |
|---|
CVE-2026-115560 Session upload progress / serialize_handler (informational) PHP.net advisory | Undetermined — more configuration evidence needed UNDETERMINED · priority=REVIEW | session.serialize_handler=(not observed); session.upload_progress.enabled=On /etc/php/8.3/fpm/php.ini | session.serialize_handler not observed in scanned configuration; runtime application behavior unknown | Inspect active php.ini for session.serialize_handler. If it is php_binary or php_serialize, or if apps call ini_set before session_start(), disable session.upload_progress.enabled until PHP's regular fix release. Static php.ini text only. PHP documents that exploitation also requires application code to ini_set serialize_handler to php before session_start(). No official fixed version list was published with the 2026-10-23 informational advisory. Not a definitive vulnerable/safe verdict. |
Runtime and process information
| Name | Location | Meaning |
|---|
| php-fpm8.3 | PID=2214
| PROCESS_NAME_ONLY_ACTIVE_HTTP_SAPI_AND_PATCH_STATUS_UNVERIFIED |
| php-fpm8.3 | /usr/sbin/php-fpm8.3
| STATIC_BINARY_VERSION_NOT_ACTIVE_RUNTIME_ATTESTATION |
| php-fpm | /opt/legacy-app/sbin/php-fpm
| STATIC_BINARY_VERSION_NOT_ACTIVE_RUNTIME_ATTESTATION |
PHP configuration findings
php.ini candidates are supplementary discovery only. They do not confirm installation, version, or active HTTP runtime. Skipped candidates list path and reason without secrets.
| Setting / file | Detail | Meaning |
|---|
session.upload_progress.enabled /etc/php/8.3/fpm/php.ini | On | CONFIGURATION_OBSERVATION_ONLY |
Vulnerability and patch assessment details
| Component | Version | Status | Minimum fixed | Action |
|---|
php8.3-fpm installed_package | 8.3.24 confidence=MEDIUM | Update review required | 8.3.36 | Update the PHP web-serving runtime to 8.3.36 or later on this supported branch (or verify an equivalent vendor security backport), restart every PHP worker and verify the serving runtime. Static inventory only; not attestation of the serving HTTP runtime. A vendor may have backported a fix without changing the upstream version string. Version below the upstream fix; vendor backports and actual deployment remain unverified. |
php8.3-cli installed_package | 8.3.24 confidence=MEDIUM | Update review required | 8.3.36 | Update the PHP web-serving runtime to 8.3.36 or later on this supported branch (or verify an equivalent vendor security backport), restart every PHP worker and verify the serving runtime. Static inventory only; not attestation of the serving HTTP runtime. A vendor may have backported a fix without changing the upstream version string. Version below the upstream fix; vendor backports and actual deployment remain unverified. |
php8.3-common installed_package | 8.3.24 confidence=MEDIUM | Update review required | 8.3.36 | Update the PHP web-serving runtime to 8.3.36 or later on this supported branch (or verify an equivalent vendor security backport), restart every PHP worker and verify the serving runtime. Static inventory only; not attestation of the serving HTTP runtime. A vendor may have backported a fix without changing the upstream version string. Version below the upstream fix; vendor backports and actual deployment remain unverified. |
php8.4-fpm installed_package | 8.4.11 confidence=MEDIUM | Update review required | 8.4.27 | Update the PHP web-serving runtime to 8.4.27 or later on this supported branch (or verify an equivalent vendor security backport), restart every PHP worker and verify the serving runtime. Static inventory only; not attestation of the serving HTTP runtime. A vendor may have backported a fix without changing the upstream version string. Version below the upstream fix; vendor backports and actual deployment remain unverified. |
php-fpm8.3 binary_static_version | 8.3.24 confidence=HIGH | Update review required | 8.3.36 | Update the PHP web-serving runtime to 8.3.36 or later on this supported branch (or verify an equivalent vendor security backport), restart every PHP worker and verify the serving runtime. Verified from passive binary metadata (embedded string and/or PE resource); not proof the binary serves HTTP or is the active runtime. Version below the upstream fix; vendor backports and actual deployment remain unverified. |
php-fpm binary_static_version | 8.3.24 confidence=HIGH | Update review required | 8.3.36 | Update the PHP web-serving runtime to 8.3.36 or later on this supported branch (or verify an equivalent vendor security backport), restart every PHP worker and verify the serving runtime. Verified from passive binary metadata (embedded string and/or PE resource); not proof the binary serves HTTP or is the active runtime. Version below the upstream fix; vendor backports and actual deployment remain unverified. |
Dockerfile repository_declaration | 8.2.34 confidence=MEDIUM | Update review required | 8.2.35 | Update the PHP web-serving runtime to 8.2.35 or later on this supported branch (or verify an equivalent vendor security backport), restart every PHP worker and verify the serving runtime. Source declaration only; may not match production. Version below the upstream fix; vendor backports and actual deployment remain unverified. |
Extended remediation notes
- URGENT: Observed installed or operator-reported PHP version below the upstream fixes. Patch all relevant running web SAPIs, not only the CLI binary. Confirm vendor backports where applicable.
- Review Dockerfiles, CI and deployment manifests naming versions below the fixes. Verify actual deployed image digest/runtime before assigning vulnerability status.
- Immediately prioritize internet-reachable mod_php and other web-facing PHP (FPM, CGI, FrankenPHP, LiteSpeed). Determine network reachability separately; this is NOT an external exposure scan.
- Apply upstream PHP 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 or your vendor's verified equivalent patches. Restart/recycle all PHP workers, pools and services; re-scan and verify the active web-serving SAPI and container images.
- If patching must be delayed: consider administratively enforced memory_limit=-1, conservative post_max_size and edge request bounds for CVE-2026-109618; these are incomplete risk reductions and have availability tradeoffs.
- For CVE-2026-109619, administratively disable session.upload_progress.enabled and restart all workers; on PHP 8.5, consider fatal_error_backtraces=0 until patched. Neither measure covers every attack path.
- Confirm WAF/origin access cannot be bypassed and document time-bounded restrictions if a patch cannot be deployed immediately.
- Preserve and review access/error logs and EDR telemetry from before patching; patched servers may already have been compromised. A clean IOC result does not rule out compromise.
- See official advisories: https://www.php.net/ and https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w and https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp
Compromise investigation (attempts, coverage, signed IOCs)
Exploitation attempts: NOT_ASSESSED · Possible compromise: NOT_ASSESSED · Investigation coverage: NOT_REQUESTED
Statuses are independent. This tool never claims a host is clean or confirmed compromised. Long request-target length, large HTTP bodies, unusual form-field names, and multipart nesting are distinct signals; ordinary access logs typically provide only the first (URI length heuristic). Blocked WAF actions are not successful exploitation. WAF leads do not alone set possible-compromise. Do not claim two requests shared a PHP worker without worker identity evidence.
Configured SAPI hints: Observed configured/inventory hints: PHP-FPM (binary/package/process or FastCGI proxy hints). These artifacts do NOT prove which SAPI currently serves HTTP. Research note: mod_php may continue into request shutdown / worker reuse after some startup fatals; PHP-FPM typically terminates the worker on request-startup failure but can still be exploited after successful startup (application-dependent decode and upload-progress paths). Do not infer that PHP-FPM cannot be exploited. Patch and verify the active serving runtime separately.
HTTP body / form metadata: Not available from ordinary access logs. Prefer offline WAF exports with documented fields. Content-Length and multipart nesting are not inferred from URI lines.
Observed PHP configuration: Whitelisted php.ini settings are static text only (OBSERVED≠EFFECTIVE). Temporary mitigations such as session.upload_progress.enabled or memory_limit changes are unverified and do not replace patching.
No trusted signed indicator bundle was loaded for this scan. Signed-IOC rollup cannot clear the host.
No signed-IOC matches were recorded in the assessed scope.
Scan coverage and limitations
| Check | Status | Notes |
|---|
| packages | PARTIAL | Read 1 supported package databases; 6 PHP-related packages |
| processes | PARTIAL | Observed matching PHP process names; scanner self-process excluded |
| active_http_sapi_verification | NOT_IMPLEMENTED | Package/process/path observations cannot verify the PHP runtime actually serving HTTP traffic |
| official_php2shell_version_rules | APPLIED | Upstream fixes: 8.2.35, 8.3.36, 8.4.27, 8.5.12. Compared 7 evidence records across 5 correlated install groups; NOT vendor backport or active HTTP runtime verification |
Warnings
- Signed indicator rules not loaded
Full technical evidence (11)
| Type | Name | Version | Source / path | Meaning |
|---|
| installed_package | php8.3-fpm | 8.3.24 | dpkg installed package metadata /var/lib/dpkg/status Package version: 8.3.24-1ubuntu1 | PACKAGE_VERSION_ONLY_VENDOR_BACKPORT_AND_ACTIVE_SAPI_UNVERIFIED |
| installed_package | php8.3-cli | 8.3.24 | dpkg installed package metadata /var/lib/dpkg/status Package version: 8.3.24-1ubuntu1 | PACKAGE_VERSION_ONLY_VENDOR_BACKPORT_AND_ACTIVE_SAPI_UNVERIFIED |
| installed_package | php8.3-common | 8.3.24 | dpkg installed package metadata /var/lib/dpkg/status Package version: 8.3.24-1ubuntu1 | PACKAGE_VERSION_ONLY_VENDOR_BACKPORT_AND_ACTIVE_SAPI_UNVERIFIED |
| installed_package | php8.3-mysql | 8.3.24 | dpkg installed package metadata /var/lib/dpkg/status Package version: 8.3.24-1ubuntu1 | PACKAGE_VERSION_ONLY_VENDOR_BACKPORT_AND_ACTIVE_SAPI_UNVERIFIED |
| installed_package | php8.4-fpm | 8.4.11 | dpkg installed package metadata /var/lib/dpkg/status Package version: 8.4.11-1 | PACKAGE_VERSION_ONLY_VENDOR_BACKPORT_AND_ACTIVE_SAPI_UNVERIFIED |
| installed_package | php-common | | dpkg installed package metadata /var/lib/dpkg/status Package version: 24.04.1 | PACKAGE_METADATA_ONLY_NOT_PHP_RUNTIME_VERSION |
| running_process | php-fpm8.3 | | /proc/PID/comm snapshot PID=2214
| PROCESS_NAME_ONLY_ACTIVE_HTTP_SAPI_AND_PATCH_STATUS_UNVERIFIED |
| binary_static_version | php-fpm8.3 | 8.3.24 | well-known path static binary inspection /usr/sbin/php-fpm8.3
| STATIC_BINARY_VERSION_NOT_ACTIVE_RUNTIME_ATTESTATION |
| binary_static_version | php-fpm | 8.3.24 | well-known path static binary inspection /opt/legacy-app/sbin/php-fpm
| STATIC_BINARY_VERSION_NOT_ACTIVE_RUNTIME_ATTESTATION |
| configuration_observation | session.upload_progress.enabled | | php.ini /etc/php/8.3/fpm/php.ini On | CONFIGURATION_OBSERVATION_ONLY |
| repository_declaration | Dockerfile | 8.2.34 | dockerfile /srv/app/Dockerfile
| DECLARED_ONLY_NOT_DEPLOYED_OR_PATCH_VERIFIED |