diff --git a/internal/audit/apache.go b/internal/audit/apache.go index 3cf92fd..935dccf 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 } @@ -427,6 +450,12 @@ func (s *apacheScanState) inspectHandlerBinary(r *Report, configured string) { if !HandlerExecutableLooksLikePHP(target) && !HandlerExecutableLooksLikePHP(configured) { return } + // Skip re-inventory when the same path was already recorded (e.g. well-known / + // PATH discovery). Handler config evidence still stands; avoid duplicate install + // rows and patch assessments for one binary path. + if reportHasBinaryPathEvidence(r, target) || reportHasBinaryPathEvidence(r, configured) { + return + } if !isSafeRegularFile(target) { s.inaccessibleBinaries++ r.AddEvidence(Evidence{ @@ -444,6 +473,23 @@ func (s *apacheScanState) inspectHandlerBinary(r *Report, configured string) { AddBinaryVersionEvidence(r, target, filepath.Base(target), "Apache handler static binary inspection") } +// reportHasBinaryPathEvidence reports whether inventory already recorded this PHP binary path. +func reportHasBinaryPathEvidence(r *Report, path string) bool { + if r == nil || strings.TrimSpace(path) == "" { + return false + } + key := normalizeInstallPath(path) + for _, e := range r.Evidence { + switch e.Type { + case "binary_path", "binary_static_version", "binary_path_inferred_version": + if normalizeInstallPath(e.Location) == key { + return true + } + } + } + return false +} + // classifyApachePHPBackend extracts backend identity and whether it is remote. func classifyApachePHPBackend(dir, args string) (backend string, remote bool, localExe string) { raw := strings.TrimSpace(args) 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..e9fa70e --- /dev/null +++ b/internal/audit/wamp_discovery_test.go @@ -0,0 +1,409 @@ +package audit + +import ( + "encoding/json" + "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. + // Isolate PATH / well-known host trees so Windows CI images (e.g. c:\tools\php) cannot pollute. + isolatePHPIniHostDiscovery(t) + 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) + + // Re-enable only the Wamp64 php.ini glob under the fixture root (isolate cleared well-known). + phpIniWellKnownPatternsFn = func() []string { + return []string{filepath.Join(root, "wamp64", "bin", "php", "*", "php.ini")} + } + scanConfigs(r, HostScanOptions{}, nil) + + EvaluatePatchPosture(r) + FinalizeSummary(r) + + inferred, verified, unknown := 0, 0, 0 + binLocs := map[string]int{} + sawApacheHandler, sawApacheMod, sawIni := false, false, false + 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) + } + if !strings.Contains(e.Detail, "provenance=path_inferred") || !strings.Contains(e.Detail, "confidence=MEDIUM") { + t.Fatal(e.Detail) + } + if e.Interpretation != "PATH_INFERRED_VERSION_NOT_BINARY_VERIFIED_NOT_RUNTIME_ATTESTATION" { + t.Fatal(e.Interpretation) + } + binLocs[normalizeInstallPath(e.Location)]++ + case "binary_static_version": + verified++ + binLocs[normalizeInstallPath(e.Location)]++ + case "binary_path": + unknown++ + binLocs[normalizeInstallPath(e.Location)]++ + case "apache_php_handler_config": + sawApacheHandler = true + case "apache_php_module_config": + sawApacheMod = true + case "php_ini_candidate": + if normalizeInstallPath(e.Location) == normalizeInstallPath(phpIni) { + sawIni = true + } + } + } + // Exactly one inventory row per binary path: well-known discovery records php.exe + + // php-cgi.exe; Apache handler follow-up must not duplicate php-cgi.exe. + if inferred != 2 || verified != 0 || unknown != 0 { + t.Fatalf("inferred=%d verified=%d unknown=%d evidence=%+v", inferred, verified, unknown, r.Evidence) + } + for loc, n := range binLocs { + if n != 1 { + t.Fatalf("duplicate binary inventory for %s count=%d", loc, n) + } + } + if !sawApacheHandler || !sawApacheMod { + t.Fatalf("apache handler=%v module=%v", sawApacheHandler, sawApacheMod) + } + if !sawIni { + t.Fatal("expected wamp64 php.ini candidate") + } + + cov := CountBinaryInstallCoverage(r) + if cov.BinaryCandidates != 2 || cov.InstallDirs != 1 || cov.PathInferred != 2 || cov.Verified != 0 || cov.VersionUnknown != 0 { + t.Fatalf("%+v", cov) + } + + if r.VulnerabilityStatus != "LEGACY_PHP_VENDOR_PATCH_VERIFICATION_REQUIRED" { + t.Fatalf("status=%s assessments=%+v", r.VulnerabilityStatus, r.PatchAssessments) + } + installKeys := map[string]struct{}{} + assessLocs := map[string]struct{}{} + 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) + } + installKeys[a.InstallKey] = struct{}{} + assessLocs[normalizeInstallPath(a.Location)] = struct{}{} + } + if len(installKeys) != 1 { + t.Fatalf("expected one install group for php7.4.33 tree, got %v", installKeys) + } + if len(assessLocs) != 2 { + t.Fatalf("expected two patch rows (php.exe + php-cgi.exe), got %v", assessLocs) + } + + 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") + } + 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") + } + + jsonBytes, err := json.Marshal(r) + if err != nil { + t.Fatal(err) + } + js := string(jsonBytes) + if !strings.Contains(js, `"type":"binary_path_inferred_version"`) || !strings.Contains(js, "provenance=path_inferred") { + t.Fatal("JSON must retain inferred evidence type and provenance detail") + } + if !strings.Contains(js, `"schema":"`+Schema+`"`) { + t.Fatal("JSON must preserve existing report schema id") + } + if r.Summary.PHPDiscovery == "" || r.Summary.PatchReview == "" { + t.Fatal("summary schema fields must remain populated") + } + + // 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) + } +} + +func TestApacheHandlerFollowUpDoesNotDuplicateExistingBinary(t *testing.T) { + root := t.TempDir() + phpDir := filepath.Join(root, "wamp64", "bin", "php", "php7.4.33") + confDir := filepath.Join(root, "apache", "conf") + _ = os.MkdirAll(phpDir, 0755) + _ = os.MkdirAll(confDir, 0755) + cgi := filepath.Join(phpDir, "php-cgi.exe") + _ = os.WriteFile(cgi, []byte("MZ\x00 marker-free"), 0644) + _ = os.WriteFile(filepath.Join(confDir, "httpd.conf"), []byte( + `Action application/x-httpd-php "`+filepath.ToSlash(cgi)+`" +`), 0644) + + r := NewReport("dedup", "windows", "amd64") + AddBinaryVersionEvidence(r, cgi, "php-cgi.exe", "well-known") + if !reportHasBinaryPathEvidence(r, cgi) { + t.Fatal("precondition") + } + ParseApacheConfigForTest(r, confDir) + + inferred := 0 + for _, e := range r.Evidence { + if e.Type == "binary_path_inferred_version" { + inferred++ + } + } + if inferred != 1 { + t.Fatalf("Apache follow-up must not re-add same binary path, got inferred=%d evidence=%+v", inferred, r.Evidence) + } + sawHandler := false + for _, e := range r.Evidence { + if e.Type == "apache_php_handler_config" { + sawHandler = true + } + } + if !sawHandler { + t.Fatal("handler config evidence must still be recorded") + } +} 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}} -

PHP2Shell Security Assessment

Host: app-web-01.example.local  ·  Assessed (UTC): 2026-10-21 16:05:00  ·  Scanner: 2.8.1

Overall priority: High — update review required

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

  1. 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.
  2. 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.
  3. 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.
  4. 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 versionMinimum fixedAssessmentLifecycle (informational)
PHP 8.2.348.2.35Update review requiredPHP 8.2: security-only until 31 Dec 2026 (php.net) — plan upgrade
PHP 8.3.24 @ /usr/sbin/php-fpm8.38.3.36Update review requiredPHP 8.3: security-only until 31 Dec 2027 (php.net)
PHP 8.3.248.3.36Update review requiredPHP 8.3: security-only until 31 Dec 2027 (php.net)
PHP 8.3.24 @ /opt/legacy-app/sbin/php-fpm8.3.36Update review requiredPHP 8.3: security-only until 31 Dec 2027 (php.net)
PHP 8.4.118.4.27Update review requiredPHP 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

ComponentTypeVersionAction / limitations
Dockerfilerepository_declaration8.2.34Update 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):

NameVersionTypeSource
Dockerfile8.2.34repository_declarationdockerfile
PHP 8.3.24 @ /usr/sbin/php-fpm8.3 — packages, binaries and evidence

Assessment: Update review required  ·  Priority: CRITICAL

ComponentTypeVersionAction / limitations
php8.3-fpminstalled_package8.3.24Update 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.3binary_static_version8.3.24Update 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):

NameVersionTypeSource
php-fpm8.38.3.24binary_static_versionwell-known path static binary inspection
PHP 8.3.24 — packages, binaries and evidence

Assessment: Update review required  ·  Priority: CRITICAL

ComponentTypeVersionAction / limitations
php8.3-cliinstalled_package8.3.24Update 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-commoninstalled_package8.3.24Update 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):

NameVersionTypeSource
php8.3-fpm8.3.24installed_packagedpkg installed package metadata
php8.3-cli8.3.24installed_packagedpkg installed package metadata
php8.3-common8.3.24installed_packagedpkg installed package metadata
php8.3-mysql8.3.24installed_packagedpkg installed package metadata
PHP 8.3.24 @ /opt/legacy-app/sbin/php-fpm — packages, binaries and evidence

Assessment: Update review required  ·  Priority: CRITICAL

ComponentTypeVersionAction / limitations
php-fpmbinary_static_version8.3.24Update 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):

NameVersionTypeSource
php-fpm8.3.24binary_static_versionwell-known path static binary inspection
PHP 8.4.11 — packages, binaries and evidence

Assessment: Update review required  ·  Priority: CRITICAL

ComponentTypeVersionAction / limitations
php8.4-fpminstalled_package8.4.11Update 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):

NameVersionTypeSource
php8.4-fpm8.4.11installed_packagedpkg 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.

CVEStatusObserved configurationMissing evidenceRecommended 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 unknownInspect 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
NameLocationMeaning
php-fpm8.3PID=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 / fileDetailMeaning
session.upload_progress.enabled
/etc/php/8.3/fpm/php.ini
OnCONFIGURATION_OBSERVATION_ONLY
Vulnerability and patch assessment details
ComponentVersionStatusMinimum fixedAction
php8.3-fpm
installed_package
8.3.24
confidence=MEDIUM
Update review required8.3.36Update 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 required8.3.36Update 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 required8.3.36Update 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 required8.4.27Update 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 required8.3.36Update 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 required8.3.36Update 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 required8.2.35Update 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

  1. 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.
  2. Review Dockerfiles, CI and deployment manifests naming versions below the fixes. Verify actual deployed image digest/runtime before assigning vulnerability status.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. Confirm WAF/origin access cannot be bypassed and document time-bounded restrictions if a patch cannot be deployed immediately.
  8. 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.
  9. 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
CheckStatusNotes
packagesPARTIALRead 1 supported package databases; 6 PHP-related packages
processesPARTIALObserved matching PHP process names; scanner self-process excluded
active_http_sapi_verificationNOT_IMPLEMENTEDPackage/process/path observations cannot verify the PHP runtime actually serving HTTP traffic
official_php2shell_version_rulesAPPLIEDUpstream 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)
TypeNameVersionSource / pathMeaning
installed_packagephp8.3-fpm8.3.24dpkg installed package metadata
/var/lib/dpkg/status
Package version: 8.3.24-1ubuntu1
PACKAGE_VERSION_ONLY_VENDOR_BACKPORT_AND_ACTIVE_SAPI_UNVERIFIED
installed_packagephp8.3-cli8.3.24dpkg installed package metadata
/var/lib/dpkg/status
Package version: 8.3.24-1ubuntu1
PACKAGE_VERSION_ONLY_VENDOR_BACKPORT_AND_ACTIVE_SAPI_UNVERIFIED
installed_packagephp8.3-common8.3.24dpkg installed package metadata
/var/lib/dpkg/status
Package version: 8.3.24-1ubuntu1
PACKAGE_VERSION_ONLY_VENDOR_BACKPORT_AND_ACTIVE_SAPI_UNVERIFIED
installed_packagephp8.3-mysql8.3.24dpkg installed package metadata
/var/lib/dpkg/status
Package version: 8.3.24-1ubuntu1
PACKAGE_VERSION_ONLY_VENDOR_BACKPORT_AND_ACTIVE_SAPI_UNVERIFIED
installed_packagephp8.4-fpm8.4.11dpkg installed package metadata
/var/lib/dpkg/status
Package version: 8.4.11-1
PACKAGE_VERSION_ONLY_VENDOR_BACKPORT_AND_ACTIVE_SAPI_UNVERIFIED
installed_packagephp-commondpkg installed package metadata
/var/lib/dpkg/status
Package version: 24.04.1
PACKAGE_METADATA_ONLY_NOT_PHP_RUNTIME_VERSION
running_processphp-fpm8.3/proc/PID/comm snapshot
PID=2214
PROCESS_NAME_ONLY_ACTIVE_HTTP_SAPI_AND_PATCH_STATUS_UNVERIFIED
binary_static_versionphp-fpm8.38.3.24well-known path static binary inspection
/usr/sbin/php-fpm8.3
STATIC_BINARY_VERSION_NOT_ACTIVE_RUNTIME_ATTESTATION
binary_static_versionphp-fpm8.3.24well-known path static binary inspection
/opt/legacy-app/sbin/php-fpm
STATIC_BINARY_VERSION_NOT_ACTIVE_RUNTIME_ATTESTATION
configuration_observationsession.upload_progress.enabledphp.ini
/etc/php/8.3/fpm/php.ini
On
CONFIGURATION_OBSERVATION_ONLY
repository_declarationDockerfile8.2.34dockerfile
/srv/app/Dockerfile
DECLARED_ONLY_NOT_DEPLOYED_OR_PATCH_VERIFIED
\ No newline at end of file +

PHP2Shell Security Assessment

Host: app-web-01.example.local  ·  Assessed (UTC): 2026-10-21 16:05:00  ·  Scanner: 2.8.1

Overall priority: High — update review required

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. 2 PHP binary candidate(s) across 2 install director(ies): 2 with verified binary metadata, 0 path-inferred (MEDIUM confidence; not binary-verified), 0 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.

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

  1. 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.
  2. 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.
  3. 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.
  4. 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 versionMinimum fixedAssessmentLifecycle (informational)
PHP 8.2.348.2.35Update review requiredPHP 8.2: security-only until 31 Dec 2026 (php.net) — plan upgrade
PHP 8.3.24 @ /usr/sbin/php-fpm8.38.3.36Update review requiredPHP 8.3: security-only until 31 Dec 2027 (php.net)
PHP 8.3.248.3.36Update review requiredPHP 8.3: security-only until 31 Dec 2027 (php.net)
PHP 8.3.24 @ /opt/legacy-app/sbin/php-fpm8.3.36Update review requiredPHP 8.3: security-only until 31 Dec 2027 (php.net)
PHP 8.4.118.4.27Update review requiredPHP 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

ComponentTypeVersionAction / limitations
Dockerfilerepository_declaration8.2.34Update 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):

NameVersionTypeSource
Dockerfile8.2.34repository_declarationdockerfile
PHP 8.3.24 @ /usr/sbin/php-fpm8.3 — packages, binaries and evidence

Assessment: Update review required  ·  Priority: CRITICAL

ComponentTypeVersionAction / limitations
php8.3-fpminstalled_package8.3.24Update 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.3binary_static_version8.3.24Update 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):

NameVersionTypeSource
php-fpm8.38.3.24binary_static_versionwell-known path static binary inspection
PHP 8.3.24 — packages, binaries and evidence

Assessment: Update review required  ·  Priority: CRITICAL

ComponentTypeVersionAction / limitations
php8.3-cliinstalled_package8.3.24Update 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-commoninstalled_package8.3.24Update 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):

NameVersionTypeSource
php8.3-fpm8.3.24installed_packagedpkg installed package metadata
php8.3-cli8.3.24installed_packagedpkg installed package metadata
php8.3-common8.3.24installed_packagedpkg installed package metadata
php8.3-mysql8.3.24installed_packagedpkg installed package metadata
PHP 8.3.24 @ /opt/legacy-app/sbin/php-fpm — packages, binaries and evidence

Assessment: Update review required  ·  Priority: CRITICAL

ComponentTypeVersionAction / limitations
php-fpmbinary_static_version8.3.24Update 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):

NameVersionTypeSource
php-fpm8.3.24binary_static_versionwell-known path static binary inspection
PHP 8.4.11 — packages, binaries and evidence

Assessment: Update review required  ·  Priority: CRITICAL

ComponentTypeVersionAction / limitations
php8.4-fpminstalled_package8.4.11Update 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):

NameVersionTypeSource
php8.4-fpm8.4.11installed_packagedpkg 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.

CVEStatusObserved configurationMissing evidenceRecommended 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 unknownInspect 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
NameLocationMeaning
php-fpm8.3PID=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 / fileDetailMeaning
session.upload_progress.enabled
/etc/php/8.3/fpm/php.ini
OnCONFIGURATION_OBSERVATION_ONLY
Vulnerability and patch assessment details
ComponentVersionStatusMinimum fixedAction
php8.3-fpm
installed_package
8.3.24
confidence=MEDIUM
Update review required8.3.36Update 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 required8.3.36Update 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 required8.3.36Update 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 required8.4.27Update 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 required8.3.36Update 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 required8.3.36Update 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 required8.2.35Update 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

  1. 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.
  2. Review Dockerfiles, CI and deployment manifests naming versions below the fixes. Verify actual deployed image digest/runtime before assigning vulnerability status.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. Confirm WAF/origin access cannot be bypassed and document time-bounded restrictions if a patch cannot be deployed immediately.
  8. 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.
  9. 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
CheckStatusNotes
packagesPARTIALRead 1 supported package databases; 6 PHP-related packages
processesPARTIALObserved matching PHP process names; scanner self-process excluded
active_http_sapi_verificationNOT_IMPLEMENTEDPackage/process/path observations cannot verify the PHP runtime actually serving HTTP traffic
official_php2shell_version_rulesAPPLIEDUpstream 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)
TypeNameVersionSource / pathMeaning
installed_packagephp8.3-fpm8.3.24dpkg installed package metadata
/var/lib/dpkg/status
Package version: 8.3.24-1ubuntu1
PACKAGE_VERSION_ONLY_VENDOR_BACKPORT_AND_ACTIVE_SAPI_UNVERIFIED
installed_packagephp8.3-cli8.3.24dpkg installed package metadata
/var/lib/dpkg/status
Package version: 8.3.24-1ubuntu1
PACKAGE_VERSION_ONLY_VENDOR_BACKPORT_AND_ACTIVE_SAPI_UNVERIFIED
installed_packagephp8.3-common8.3.24dpkg installed package metadata
/var/lib/dpkg/status
Package version: 8.3.24-1ubuntu1
PACKAGE_VERSION_ONLY_VENDOR_BACKPORT_AND_ACTIVE_SAPI_UNVERIFIED
installed_packagephp8.3-mysql8.3.24dpkg installed package metadata
/var/lib/dpkg/status
Package version: 8.3.24-1ubuntu1
PACKAGE_VERSION_ONLY_VENDOR_BACKPORT_AND_ACTIVE_SAPI_UNVERIFIED
installed_packagephp8.4-fpm8.4.11dpkg installed package metadata
/var/lib/dpkg/status
Package version: 8.4.11-1
PACKAGE_VERSION_ONLY_VENDOR_BACKPORT_AND_ACTIVE_SAPI_UNVERIFIED
installed_packagephp-commondpkg installed package metadata
/var/lib/dpkg/status
Package version: 24.04.1
PACKAGE_METADATA_ONLY_NOT_PHP_RUNTIME_VERSION
running_processphp-fpm8.3/proc/PID/comm snapshot
PID=2214
PROCESS_NAME_ONLY_ACTIVE_HTTP_SAPI_AND_PATCH_STATUS_UNVERIFIED
binary_static_versionphp-fpm8.38.3.24well-known path static binary inspection
/usr/sbin/php-fpm8.3
STATIC_BINARY_VERSION_NOT_ACTIVE_RUNTIME_ATTESTATION
binary_static_versionphp-fpm8.3.24well-known path static binary inspection
/opt/legacy-app/sbin/php-fpm
STATIC_BINARY_VERSION_NOT_ACTIVE_RUNTIME_ATTESTATION
configuration_observationsession.upload_progress.enabledphp.ini
/etc/php/8.3/fpm/php.ini
On
CONFIGURATION_OBSERVATION_ONLY
repository_declarationDockerfile8.2.34dockerfile
/srv/app/Dockerfile
DECLARED_ONLY_NOT_DEPLOYED_OR_PATCH_VERIFIED
\ No newline at end of file