Build a PHP2Shell Patch Checker

Build a PowerShell tool that checks PHP patch baselines for critical CVEs.

Introduction

30 Second Summary

When a critical software warning lands, the urgent question is whether your version still needs a patch. A guess can waste response time or leave a known risk unresolved.

In this project, you will build a defensive PowerShell checker that compares a supplied PHP release with official patched baselines for two CVEs. The finished tool produces a clear status plus a Markdown report for remediation evidence.

What You'll Build

You run one command to see whether a PHP release is EXPOSED, has PATCHED BASELINE MET, or needs REVIEW REQUIRED, with a timestamped report ready for a remediation ticket.

By the end of this project, you'll have:

  • A branch-aware version assessment that screens releases across PHP 8.2, 8.3, 8.4, and 8.5.
  • Two timestamped evidence reports that demonstrate an exposed release plus a patched-baseline release for CVE-2026-109618 and CVE-2026-109619.
  • A public GitHub repository where you can show the checker, its evidence, its limitations, and its official sources.
  • Secret Mission: Add an automated eight-case boundary test covering the last affected release plus the first patched release in every supported PHP branch.

Are there any prerequisites?

You need a Windows computer with Visual Studio Code plus an internet connection. PHP is not required because the checker evaluates supplied release values through Windows PowerShell 5.1.

Before We Start

This checkpoint locks in the defensive purpose of your checker before any hands-on work begins. Your checker assesses a supplied PHP release against official branch-specific patched baselines without scanning hosts, sending traffic, or reproducing exploits.

Set Up the Portfolio Workspace

A reproducible security project starts with a known shell. Its evidence also needs a clear repository.

In this step, you will verify Windows PowerShell 5.1 inside Visual Studio Code. You will also prepare a GitHub publishing path before creating the checker.

In this step, get ready to:
  • Verify Windows PowerShell 5.1 in Visual Studio Code.
  • Verify a free GitHub account.
  • Prepare a repository workspace for later publishing.
Verify Windows PowerShell 5.1

The checker depends on features available in Windows PowerShell 5.1. The version object confirms the shell that Visual Studio Code is actually running.

  • Press the Windows key to open Windows search.
  • Type Visual Studio Code and press Enter.

You will see Visual Studio Code open on your Windows computer.

  • Select View from the top menu.
  • Select Terminal.
  • Check the running shell version by running this command:
$PSVersionTable.PSVersion

What does this command check?

The $PSVersionTable.PSVersion command reads the version object for the current PowerShell session. Its major and minor fields identify the shell release.

✔️ I see version 5.1

Your terminal is running Windows PowerShell 5.1. That gives the checker the verified shell it needs.

ⓧ I see different numbers

VS Code may have opened another PowerShell edition. Switch the terminal profile so this project uses Windows PowerShell.

  • Close the current terminal from the terminal panel.
  • Open the terminal profile selector in the terminal panel.
  • Select Windows PowerShell.
  • Repeat the version command shown above.

You should now see Major set to 5. You should also see Minor set to 1.

ⓧ The command does not run

The terminal may be using a shell that does not recognize PowerShell variables. Select the Windows-shipped shell before trying again.

  • Close the terminal that rejected the command.
  • Open the terminal profile selector in the terminal panel.
  • Select Windows PowerShell.
  • Repeat the version command shown above.

The terminal should display a version table containing major and minor values.

Still unable to verify the shell?

Make sure the terminal profile says Windows PowerShell. A profile with another name may report a different version.

Help me verify Windows PowerShell 5.1 in Visual Studio Code.

Create and verify your GitHub account

Your GitHub account holds the repository that you publish later. A verified email address makes repository creation available.

Creating the account does not start a paid subscription. GitHub Free costs $0 USD per month.

Your account credentials stay inside GitHub's sign-up flow. Project files do not need a copy of them.

  • Open the GitHub sign-up page in your browser.
  • Follow the prompts to create your personal account.
  • Open the verification message that GitHub sends to your email address.
  • Complete the email verification from that message.
  • Return to GitHub after the verification finishes.

Your verified GitHub account can now create the repository that holds your defensive checker.

Create the repository workspace

GitHub Desktop supports Windows 10 64-bit or later. The GitHub browser workflow provides a publishing fallback when the desktop app reports that Windows is unsupported.

✔️ GitHub Desktop is supported

This path creates the repository on your Desktop. GitHub Desktop manages the repository while Visual Studio Code manages its files.

GitHub Desktop launches after the setup file completes. That confirms the desktop publishing tool is installed.

  • Sign in to GitHub Desktop with your verified GitHub account.

You will see GitHub Desktop connected to your account.

  • Select Create a New Repository on your Hard Drive....
  • Set the repository name to php2shell-patch-readiness-checker.

The repository dialog now shows the project name that appears throughout the checker.

  • Choose your Desktop as the repository location.
  • Select Initialize this repository with a README.

The dialog now points to your Desktop and includes the initial documentation file.

  • Select Create repository.

GitHub Desktop now displays php2shell-patch-readiness-checker as the current repository.

  • Return to Visual Studio Code.
  • Select Open Folder... from the File menu.

You will see the Windows folder picker.

  • Select php2shell-patch-readiness-checker from your Desktop.
  • Select Open.

Visual Studio Code now shows the local repository in its file sidebar. The initialized README.md file is listed inside it.

ⓧ GitHub Desktop says Windows is unsupported

The browser fallback gives you a remote repository for publishing. A matching local project folder keeps the checker files ready for upload later.

  • Return to your verified GitHub account in the browser.
  • Select New repository.
  • Enter php2shell-patch-readiness-checker as the Repository name.

The repository form now shows the project name.

  • Select Public as the repository visibility.
  • Select Add a README file.

The repository is ready to start with a visible documentation file.

  • Select Create repository.

GitHub now displays the remote php2shell-patch-readiness-checker repository.

Why keep a local folder?

The browser repository provides the publishing destination. The local folder holds the scripts and reports that you upload in the publishing step.

  • Return to Visual Studio Code.
  • Select Open Folder... from the File menu.

You will see the Windows folder picker.

  • Create a folder named php2shell-patch-readiness-checker on your Desktop.

The project folder is now visible in the folder picker.

  • Select the new php2shell-patch-readiness-checker folder.
  • Select Open.
  • Create README.md inside the folder from Visual Studio Code's file sidebar.

Visual Studio Code now shows the local project folder with README.md inside it.

A README.md file gives the repository a visible identity. This first heading establishes the project name before the usage guide is added later.

  • Select README.md in Visual Studio Code's file sidebar.
  • Replace its contents by copying the code below:
# PHP2Shell Patch Readiness Checker

What does this file establish?

  • The Markdown heading gives the repository the same readable title as the project.
  • The filename gives GitHub a standard landing page for documentation.
  • Save README.md.
  • Confirm the Visual Studio Code editor shows PHP2Shell Patch Readiness Checker as the heading.
  • Confirm the file sidebar lists README.md inside php2shell-patch-readiness-checker.

Your repository now has its first saved project artifact.

README missing or in the wrong folder?

  • Confirm Visual Studio Code shows php2shell-patch-readiness-checker as the open folder.
  • Create README.md directly inside that folder if it appears elsewhere.
  • Help me place README.md inside my project folder.

✔️ Awesome, I've got everything!

Great. Double-check that README.md is saved inside the open project folder.

ⓧ I'd like to double check the full code

# PHP2Shell Patch Readiness Checker

What should match?

Use this full-file reference to confirm that README.md contains the project heading with the same capitalization.

Before you run the final check, make a prediction about the major and minor values.

  • Verify the active shell again by running this command:
$PSVersionTable.PSVersion

What should you see?

The Major row shows 5. The Minor row shows 1.

Those values confirm Windows PowerShell 5.1 is active.

  • Return to the repository view in GitHub Desktop if you used the desktop path.
  • Return to the repository page on GitHub if you used the browser fallback.
  • Confirm php2shell-patch-readiness-checker is visible as the repository name.

That is the workspace ready: your shell is verified, your account can publish, and your repository has its first saved file.

Your portfolio workspace is ready. Next up, you will turn one official patched baseline into a runnable version check.

Build a First Version Check

Your portfolio workspace is ready for its first working security check. Now it needs a runnable decision that turns advisory data into a visible result.

The official PHP advisories identify 8.5.12 as the first patched release in the PHP 8.5 branch. This step turns that threshold into a small Windows PowerShell decision with an immediate terminal result.

In this step, get ready to:
  • Create a script with the required PHP release input.
  • Compare the input with the PHP 8.5 patched baseline.
  • Run the checker against an exposed release.
Create the checker input and baseline

A first-pass checker needs an input to assess. It also needs a baseline that represents the first patched release for this branch.

  • Select New File... in the Explorer view in Visual Studio Code.
  • Submit Test-PHP2ShellExposure.ps1 as the file name with Enter.

The new file opens in an editor tab inside the php2shell-patch-readiness-checker repository.

  • Add the required input and PHP 8.5 baseline by pasting this code into Test-PHP2ShellExposure.ps1:
param(
    [Parameter(Mandatory = $true)]
    [string]$PhpVersion
)

$fixedBaseline = "8.5.12"

What does this code do?

  • The param() block defines the release value that the checker receives.
  • The [Parameter(Mandatory = $true)] attribute requires the caller to supply PhpVersion.
  • The $fixedBaseline variable stores the first patched release for the PHP 8.5 branch.
  • Save Test-PHP2ShellExposure.ps1.
  • Check the Explorer view. You should see Test-PHP2ShellExposure.ps1 beside README.md.

Script file missing or incomplete?

  • Confirm that the file name ends with .ps1.
  • Check that Test-PHP2ShellExposure.ps1 appears inside the php2shell-patch-readiness-checker repository.
  • Help me fix a PowerShell script file that is missing or incomplete in Visual Studio Code.
Add the first status check

The classifier needs two outcomes for this first branch check. A supplied value below the baseline maps to an exposed status.

  • Place your cursor on a new line below $fixedBaseline = "8.5.12".
  • Add the first classification by pasting this code:
if ($PhpVersion -lt $fixedBaseline) {
    Write-Output "Status: EXPOSED"
}
else {
    Write-Output "Status: PATCHED BASELINE MET"
}

How does the classification work?

  • The -lt comparison checks whether the supplied value falls below the fixed baseline.
  • The first branch writes Status: EXPOSED when the comparison is true.
  • The else branch writes Status: PATCHED BASELINE MET for every other value.
  • Save Test-PHP2ShellExposure.ps1.
  • Review the bottom of the file. You should see both status messages inside the completed if statement.

Seeing mismatched braces?

  • Confirm that the if branch has one opening brace and one closing brace.
  • Confirm that the else branch has one opening brace and one closing brace.
  • Help me find a brace or quote mismatch in my first PowerShell version checker.
Test the checker with an exposed release

A terminal run closes the loop between the advisory threshold and the script. This check supplies a PHP release immediately below the patched baseline.

Before you run this check, form a prediction about the status for 8.5.11 against the 8.5.12 baseline.

  • Invoke the checker from the Windows PowerShell terminal by running this command:
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.5.11

What does this command do?

  • The powershell.exe executable starts a Windows PowerShell session for the check.
  • The -ExecutionPolicy RemoteSigned setting applies to the new session without storing the policy in the registry.
  • The -File argument identifies the script that receives 8.5.11 through PhpVersion.

You will see Status: EXPOSED in the terminal. That is your first runnable security decision for the PHP 8.5 branch.

Why is this release exposed?

The official advisories for CVE-2026-109618 and CVE-2026-109619 list PHP 8.5 releases below 8.5.12 as affected. The supplied 8.5.11 release falls inside that range.

  • Keep the terminal output visible for your checkpoint.

Status missing or command failing?

  • Confirm that the terminal is still inside the php2shell-patch-readiness-checker repository.
  • Check that the script file is named exactly Test-PHP2ShellExposure.ps1.
  • Help me diagnose why my first PHP2Shell exposure check does not display the expected status.

The saved script should now match this complete first-pass version.

✔️ Awesome, I've got everything!

Your checker is saved with its required input, fixed baseline, and two status outcomes.

ⓧ I'd like to double check the full code

param(
    [Parameter(Mandatory = $true)]
    [string]$PhpVersion
)

$fixedBaseline = "8.5.12"

if ($PhpVersion -lt $fixedBaseline) {
    Write-Output "Status: EXPOSED"
}
else {
    Write-Output "Status: PATCHED BASELINE MET"
}

Your first checker now turns one official threshold into a visible result. Next, you will challenge this first-pass logic with a release from another PHP branch.

Expose the One-Baseline Mistake

Your first PowerShell checker accepts a PHP release. It correctly classifies 8.5.11 as exposed.

Security advisories can set a different patched release for each maintained branch. This step tests whether one stored threshold can safely represent two PHP branches.

In this step, get ready to:
  • Predict how the first checker handles the PHP 8.4 patched baseline.
  • Run a boundary check that reveals the incorrect classification.
  • Document the checker’s known limitation in README.md.
Test the PHP 8.4 boundary

The official patched baseline for PHP 8.4 is 8.4.27. Your current script still holds 8.5.12 in fixedBaseline.

  • Switch back to the Windows PowerShell terminal from the previous step.

Before you run the boundary check, consider whether the script will recognize PHP 8.4.27 as patched.

  • Test the current baseline by running this command:
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.4.27

What did the check expose?

  • The command supplies 8.4.27 as the release to assess.
  • The $fixedBaseline variable still holds 8.5.12.
  • That comparison prints Status: EXPOSED.
  • PHP 8.4.27 is the patched baseline for its branch. The displayed result is therefore incorrect.

Seeing a different result?

  • Confirm that $fixedBaseline still contains 8.5.12.
  • Confirm that the command supplies 8.4.27 to PhpVersion.

Help me diagnose why my first-pass PowerShell checker produces a different result for PHP 8.4.27.

You found the gap. The checker has treated a patched PHP 8.4 release as exposed.

Document the known limitation

A known limitation protects readers from treating a demonstrated prototype as finished triage logic. The note should identify the faulty assumption.

It should also name the observable consequence.

  • Select README.md from the repository file list in Visual Studio Code.
  • Replace the current contents of README.md with the complete file below:
# PHP2Shell Patch Readiness Checker

## Known limitation

The first version of this checker uses the PHP 8.5 fixed release as one universal baseline. Each supported PHP branch has its own fixed release, so this approach incorrectly marks PHP 8.4.27 as exposed even though 8.4.27 is the patched baseline for PHP 8.4.

What does this documentation add?

  • The Known limitation heading makes the current scope visible to readers.
  • The 8.4.27 example connects the logic error to the result you reproduced.
  • The final sentence explains why one universal baseline produces a false finding.
  • Save README.md.
  • Review the saved Known limitation section in the editor.

You should see the limitation directly below the project title. The note should name PHP 8.4.27 as the incorrectly classified release.

README note missing?

  • Confirm that you edited the README.md file inside php2shell-patch-readiness-checker.
  • Save README.md again after replacing its contents.

Help me check why my known limitation section is missing from README.md.

✔️ Awesome, I've got everything!

Your README.md now records the false result. Your first-pass checker remains unchanged for the final verification.

ⓧ I'd like to double check the full code

  • Compare your saved README.md with this complete reference:
# PHP2Shell Patch Readiness Checker

## Known limitation

The first version of this checker uses the PHP 8.5 fixed release as one universal baseline. Each supported PHP branch has its own fixed release, so this approach incorrectly marks PHP 8.4.27 as exposed even though 8.4.27 is the patched baseline for PHP 8.4.

What should match in README.md?

The complete file contains the project title plus one limitation section. Its example matches the false result from your terminal.

  • Compare the unchanged Test-PHP2ShellExposure.ps1 file with this complete reference:
param(
    [Parameter(Mandatory = $true)]
    [string]$PhpVersion
)

$fixedBaseline = "8.5.12"

if ($PhpVersion -lt $fixedBaseline) {
    Write-Output "Status: EXPOSED"
}
else {
    Write-Output "Status: PATCHED BASELINE MET"
}

What should match in the checker?

The script still accepts PhpVersion as a mandatory string. It still compares every supplied release with the single 8.5.12 baseline.

Confirm the false result remains reproducible

The documentation changed. The checker logic stayed unchanged.

  • Switch back to the Windows PowerShell terminal from earlier.

Before you repeat the check, consider whether editing README.md should change the script’s classification.

  • Confirm the designed failure remains reproducible by running this command:
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.4.27

What should you see?

  • You should see Status: EXPOSED again.
  • The repeated false result confirms that the script still measures PHP 8.4.27 against the PHP 8.5 baseline.
  • Your screenshot plus the saved limitation now preserve the designed failure as evidence.

The baseline mistake is now visible plus documented. Next up, you’ll correct the checker so each covered PHP branch is measured against its own patched release.

Add Branch-Aware CVE Classification

Your PHP 8.4.27 test exposed the cost of one universal baseline. A checker that ignores release branches can send defenders toward remediation that is already complete.

Now you will give each supported branch its own patched baseline. Structured System.Version values let Windows PowerShell compare release components in their numeric order.

In this step, get ready to:
  • Map each supported PHP branch to its official patched baseline.
  • Parse each supplied release with structured version objects.
  • Add both CVE findings to the checker output.
Validate the release input

The final checker validates the required release before it builds a version object. CmdletBinding also gives the script cmdlet-style parameter binding.

  • In Test-PHP2ShellExposure.ps1 from earlier, select the existing param() block.
  • Replace the selected block with this parameter block:
[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [string]$PhpVersion
)

What does this code do?

  • The [CmdletBinding()] attribute gives the script cmdlet-style parameter binding.
  • The [Parameter(Mandatory = $true)] attribute keeps PhpVersion required.
  • The [ValidateNotNullOrEmpty()] attribute rejects null or empty input.
  • The [string]$PhpVersion declaration preserves the dotted release as the supplied value.
  • Save Test-PHP2ShellExposure.ps1.
  • Confirm the parameter edit kept the checker runnable by running this command:
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.4.27

What does this command check?

  • The -NoProfile flag starts the new session without loading a PowerShell profile.
  • The -ExecutionPolicy RemoteSigned setting applies to this new session.
  • The -File flag runs your checker with 8.4.27 as its supplied release.

You will still see Status: EXPOSED because the branch logic has not changed. This confirms the parameter edit kept the first checker runnable.

Script stopped accepting the version?

  • Check that [ValidateNotNullOrEmpty()] sits directly above [string]$PhpVersion.
  • Confirm the closing parenthesis for param() remains below the parameter declaration.
  • Help me debug the updated PowerShell parameter block.
Classify each PHP branch

A branch-aware hashtable pairs each PHP branch with its own fixed release. Parsing the input turns version comparison into release-component comparison.

This is the most delicate edit in the step because it replaces the old comparison as one block.

  • In Test-PHP2ShellExposure.ps1, select everything from $fixedBaseline = "8.5.12" through the final closing brace of the existing else block.
  • Replace the selected block with this branch-aware classification logic:
$fixedVersions = @{
    "8.2" = [System.Version]"8.2.35"
    "8.3" = [System.Version]"8.3.36"
    "8.4" = [System.Version]"8.4.27"
    "8.5" = [System.Version]"8.5.12"
}

try {
    $parsedVersion = [System.Version]::Parse($PhpVersion)
}
catch {
    throw "PhpVersion must use a dotted release format such as 8.5.12."
}

$branch = "{0}.{1}" -f $parsedVersion.Major, $parsedVersion.Minor
$minimumFixed = $fixedVersions[$branch]

if ($null -eq $minimumFixed) {
    $status = "REVIEW REQUIRED"
}
elseif ($parsedVersion -lt $minimumFixed) {
    $status = "EXPOSED"
}
else {
    $status = "PATCHED BASELINE MET"
}

Write-Output "Status: $status"

How does branch-aware classification work?

  • The $fixedVersions hashtable uses PHP branch strings as keys.
  • Each hashtable value is a System.Version object containing the first patched release.
  • The try block parses the supplied dotted release into $parsedVersion.
  • The catch block stops malformed release values with a clear message.
  • The $branch value combines the parsed major component with the parsed minor component.
  • The $minimumFixed lookup selects the baseline for that branch.
  • An unmapped branch receives REVIEW REQUIRED.
  • The -lt comparison assigns EXPOSED when the supplied version falls below its branch baseline.
  • Save Test-PHP2ShellExposure.ps1.

Before you run the boundary checks, which result do you expect on each side of the PHP 8.4 patched baseline?

  • Test both boundary releases by running these commands:
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.4.26
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.4.27

What do these checks compare?

The commands place adjacent PHP releases on opposite sides of the 8.4.27 baseline. This boundary reveals whether the checker selected the PHP 8.4 entry from the hashtable.

You will see Status: EXPOSED for 8.4.26. You will see Status: PATCHED BASELINE MET for 8.4.27.

Both releases show the same status?

  • Check that the 8.4 key maps to 8.4.27.
  • Confirm that $parsedVersion appears on the left side of -lt.
  • Confirm that $minimumFixed appears on the right side of -lt.
  • Help me debug the branch-aware PowerShell comparison.
Add the CVE findings

The two vulnerabilities share the same patched branch baselines. Separate finding lines preserve each official CVE identity in the checker output.

  • In Test-PHP2ShellExposure.ps1, find Write-Output "Status: $status".
  • Replace that line with this three-line output group:
Write-Output "Status: $status"
Write-Output "CVE-2026-109618 | RCE via OOM corruption | Critical, CVSS 4.0 score 9.2 | https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w"
Write-Output "CVE-2026-109619 | RCE via stale global state | Critical, CVSS 4.0 score 9.2 | https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp"

What does the output model include?

  • The first line preserves the branch-aware status.
  • The second line identifies CVE-2026-109618 with its official finding name.
  • The third line identifies CVE-2026-109619 with its official finding name.
  • Each finding line includes its Critical severity score.
  • Each finding line includes its official advisory URL.
  • Save Test-PHP2ShellExposure.ps1.

Before you run the finished checker, do you expect the corrected branch logic to preserve the two different boundary results?

  • Verify the finished classification by running both commands again:
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.4.26
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.4.27

What does the final rerun prove?

  • The first run checks the final affected PHP 8.4 release against its branch baseline.
  • The second run checks the first patched PHP 8.4 release against the same baseline.
  • Both runs print the two official CVE finding records.

That closes the classification gap you exposed earlier. PHP 8.4.26 now returns EXPOSED.

PHP 8.4.27 now returns PATCHED BASELINE MET. Each run also displays both CVE records with their official advisory URLs.

Status works but findings are missing?

  • Confirm both CVE output lines sit below Write-Output "Status: $status".
  • Check that every Write-Output string has an opening double quote.
  • Check that every Write-Output string has a closing double quote.
  • Help me restore the missing CVE output lines.

Your finished script should match this reference.

✔️ Awesome, I've got everything!

Your branch-aware checker is saved with both CVE findings.

ⓧ I'd like to double check the full code

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [string]$PhpVersion
)

$fixedVersions = @{
    "8.2" = [System.Version]"8.2.35"
    "8.3" = [System.Version]"8.3.36"
    "8.4" = [System.Version]"8.4.27"
    "8.5" = [System.Version]"8.5.12"
}

try {
    $parsedVersion = [System.Version]::Parse($PhpVersion)
}
catch {
    throw "PhpVersion must use a dotted release format such as 8.5.12."
}

$branch = "{0}.{1}" -f $parsedVersion.Major, $parsedVersion.Minor
$minimumFixed = $fixedVersions[$branch]

if ($null -eq $minimumFixed) {
    $status = "REVIEW REQUIRED"
}
elseif ($parsedVersion -lt $minimumFixed) {
    $status = "EXPOSED"
}
else {
    $status = "PATCHED BASELINE MET"
}

Write-Output "Status: $status"
Write-Output "CVE-2026-109618 | RCE via OOM corruption | Critical, CVSS 4.0 score 9.2 | https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w"
Write-Output "CVE-2026-109619 | RCE via stale global state | Critical, CVSS 4.0 score 9.2 | https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp"

Your checker now handles all four covered PHP branches with structured version comparison. Next up, you will turn these classifications into durable Markdown evidence.

Generate Evidence and Publish

Your branch-aware PowerShell checker now handles all four supported PHP branches with structured version comparisons. The next goal is to preserve each result as evidence.

A terminal result is temporary. You will preserve each assessment in a Markdown report. You will publish the checker through GitHub so someone else can review its purpose and limitations.

In this step, get ready to:
  • Extend the checker with report metadata and remediation guidance.
  • Generate two evidence reports and complete the project documentation.
  • Publish the defensive checker in a public repository.
Add durable Markdown reporting

The report path gives each assessment a durable destination. A default path keeps the checker convenient while the optional parameter lets you name evidence for a specific release.

  • In Test-PHP2ShellExposure.ps1, locate the existing param() block.
  • Replace that block with the following parameter definition:
[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [string]$PhpVersion,

    [ValidateNotNullOrEmpty()]
    [string]$ReportPath = ".\php2shell-report.md"
)

What Does This Parameter Add?

The existing PhpVersion parameter remains mandatory. The new ReportPath parameter accepts a custom evidence filename.

The default writes to .\php2shell-report.md when no custom path is supplied.

A useful report also explains the branch baseline behind the result. It gives the reviewer a specific remediation path for every classification.

  • Locate the conditional block that begins with if ($null -eq $minimumFixed).
  • Replace the complete conditional block with this expanded version:
if ($null -eq $minimumFixed) {
    $status = "REVIEW REQUIRED"
    $baselineText = "Not mapped by this checker"
    $remediation = "Consult the current PHP security advisory for this branch or vendor-supplied backport information."
}
elseif ($parsedVersion -lt $minimumFixed) {
    $status = "EXPOSED"
    $baselineText = $minimumFixed.ToString()
    $remediation = "Upgrade to the patched release for this branch or to a current supported PHP release."
}
else {
    $status = "PATCHED BASELINE MET"
    $baselineText = $minimumFixed.ToString()
    $remediation = "The supplied release meets the official patched baseline for this branch. Continue normal patch management and verify vendor-specific packages separately."
}

What Does This Logic Add?

  • The baselineText value records the branch-specific release used for the assessment.
  • The remediation value gives the reviewer an action that matches the classification.
  • An unmapped branch receives manual-review guidance instead of an unsupported baseline.
  • Save Test-PHP2ShellExposure.ps1.
  • Confirm the corrected classification still runs by executing this command:
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.4.27

What Should You See?

You should see Status: PATCHED BASELINE MET. This confirms the expanded branch logic preserves the corrected PHP 8.4 boundary.

Seeing a Script Error?

Check that $PhpVersion ends with a comma before the ReportPath parameter. Confirm that the replacement conditional has one closing brace for each branch.

Ask for help with the exact line PowerShell highlights: Why does my PowerShell checker fail after adding ReportPath and remediation values?

A report needs a timestamp plus the values calculated by the checker. The first report template captures the assessment summary and both official findings.

  • Locate the three Write-Output lines at the bottom of Test-PHP2ShellExposure.ps1.
  • Replace those lines with this initial report template:
$checkedAt = Get-Date -Format "yyyy-MM-dd HH:mm K"

$report = @"
# PHP2Shell Patch Readiness Report

- Checked: $checkedAt
- Supplied PHP release: $PhpVersion
- Parsed branch: $branch
- Branch patched baseline: $baselineText
- Status: $status

## Findings

| CVE | Official finding | Severity | Advisory |
| --- | --- | --- | --- |
| CVE-2026-109618 | RCE via OOM corruption | Critical, CVSS 4.0 score 9.2 | https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w |
| CVE-2026-109619 | RCE via stale global state | Critical, CVSS 4.0 score 9.2 | https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp |
"@

Set-Content -Path $ReportPath -Value $report -Encoding UTF8

Write-Output "Status: $status"
Write-Output "Report: $ReportPath"

How Does the Report Work?

  • Get-Date records when the assessment was created.
  • The expandable string inserts the supplied release and calculated branch values into Markdown.
  • Set-Content writes the completed report using UTF-8 encoding.
  • The final terminal lines identify the classification and saved report path.

The findings table identifies the vulnerabilities. The remaining sections explain the response and the limits of this version-based assessment.

  • Locate the closing "@ below the second CVE row.
  • Insert the following report sections immediately above that closing line:
## Remediation

$remediation

PHP states that updating to a release containing the patch is the only complete mitigation for these vulnerabilities.

## Scope

This is a version-based screening result, not an exploit test. It does not inspect a live server, vendor backports, PHP configuration, web-server SAPI, application reachability, or compensating controls.

## Official sources

- PHP security release archive: https://www.php.net/archive/2026.php
- CVE-2026-109618 advisory: https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w
- CVE-2026-109619 advisory: https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp

Why Include Scope and Sources?

The remediation section connects the classification to a defensive action. The scope section prevents a version result from being mistaken for a live-server assessment.

The official links let a reviewer trace each baseline and finding back to its authoritative source.

  • Save Test-PHP2ShellExposure.ps1.

Before you run the completed checker, what status should a supplied release of 8.5.11 receive against the PHP 8.5 patched baseline?

  • Generate the first complete report by running this command:
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.5.11 -ReportPath .\exposed-8.5.11.md

What Should You See?

You should see Status: EXPOSED followed by Report: .\exposed-8.5.11.md.

The new report file proves the terminal result now survives as reviewable evidence.

Report File Missing?

Confirm that Set-Content appears after the closing "@ line. Check that the command runs from the repository containing the script.

Ask for help with the report-writing section: Why did my checker print a status without creating the Markdown report?

Use these tabs to compare your completed script with the required file.

✔️ Awesome, I've got everything!

Your checker now produces a timestamped UTF-8 Markdown report with remediation and scope details.

ⓧ I'd like to double check the full code

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [string]$PhpVersion,

    [ValidateNotNullOrEmpty()]
    [string]$ReportPath = ".\php2shell-report.md"
)

$fixedVersions = @{
    "8.2" = [System.Version]"8.2.35"
    "8.3" = [System.Version]"8.3.36"
    "8.4" = [System.Version]"8.4.27"
    "8.5" = [System.Version]"8.5.12"
}

try {
    $parsedVersion = [System.Version]::Parse($PhpVersion)
}
catch {
    throw "PhpVersion must use a dotted release format such as 8.5.12."
}

$branch = "{0}.{1}" -f $parsedVersion.Major, $parsedVersion.Minor
$minimumFixed = $fixedVersions[$branch]

if ($null -eq $minimumFixed) {
    $status = "REVIEW REQUIRED"
    $baselineText = "Not mapped by this checker"
    $remediation = "Consult the current PHP security advisory for this branch or vendor-supplied backport information."
}
elseif ($parsedVersion -lt $minimumFixed) {
    $status = "EXPOSED"
    $baselineText = $minimumFixed.ToString()
    $remediation = "Upgrade to the patched release for this branch or to a current supported PHP release."
}
else {
    $status = "PATCHED BASELINE MET"
    $baselineText = $minimumFixed.ToString()
    $remediation = "The supplied release meets the official patched baseline for this branch. Continue normal patch management and verify vendor-specific packages separately."
}

$checkedAt = Get-Date -Format "yyyy-MM-dd HH:mm K"

$report = @"
# PHP2Shell Patch Readiness Report

- Checked: $checkedAt
- Supplied PHP release: $PhpVersion
- Parsed branch: $branch
- Branch patched baseline: $baselineText
- Status: $status

## Findings

| CVE | Official finding | Severity | Advisory |
| --- | --- | --- | --- |
| CVE-2026-109618 | RCE via OOM corruption | Critical, CVSS 4.0 score 9.2 | https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w |
| CVE-2026-109619 | RCE via stale global state | Critical, CVSS 4.0 score 9.2 | https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp |

## Remediation

$remediation

PHP states that updating to a release containing the patch is the only complete mitigation for these vulnerabilities.

## Scope

This is a version-based screening result, not an exploit test. It does not inspect a live server, vendor backports, PHP configuration, web-server SAPI, application reachability, or compensating controls.

## Official sources

- PHP security release archive: https://www.php.net/archive/2026.php
- CVE-2026-109618 advisory: https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w
- CVE-2026-109619 advisory: https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp
"@

Set-Content -Path $ReportPath -Value $report -Encoding UTF8

Write-Output "Status: $status"
Write-Output "Report: $ReportPath"

How to Compare the File

Check the parameter block first. Compare the classification block next. Finish with the complete report template and the two output lines.

Document and generate the evidence

The README gives a reviewer enough context to run the checker responsibly. It also makes the safe scope visible before anyone copies a command.

  • Switch back to README.md in Visual Studio Code.
  • Replace the current contents with the following opening sections:
# PHP2Shell Patch Readiness Checker

A defensive PowerShell tool that screens a supplied PHP release against the official patched baselines for CVE-2026-109618 and CVE-2026-109619.

## What it does

- Accepts a dotted PHP release such as `8.5.11`.
- Selects the official patched baseline for PHP 8.2, 8.3, 8.4, or 8.5.
- Returns `EXPOSED`, `PATCHED BASELINE MET`, or `REVIEW REQUIRED`.
- Writes a Markdown assessment with remediation, limitations, and official sources.

## Safe scope

This project performs version-based screening only. It does not scan hosts, send web requests, reproduce memory corruption, or execute exploit code.

## Run an exposed example

```powershell
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.5.11 -ReportPath .\exposed-8.5.11.md
```

Expected status: `EXPOSED`

## Run a patched-baseline example

```powershell
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.5.12 -ReportPath .\patched-8.5.12.md
```

Expected status: `PATCHED BASELINE MET`

What Does This Documentation Establish?

The opening explains the checker in defensive terms. The safe-scope section states the actions the project never performs.

The two example commands make the exposed and patched outcomes reproducible.

  • Save README.md.

You should now see the project purpose and both runnable examples in the editor.

Markdown Fences Look Broken?

Confirm that each PowerShell command sits between one opening fence and one closing fence. Keep a blank line before each expected-status line.

Ask for help checking the structure: Why are the PowerShell commands rendering incorrectly in my README?

The second half records the exact branch matrix and interpretation rules. It closes with the limitations and sources a reviewer needs for independent verification.

  • Place the cursor after the patched example in README.md.
  • Append the remaining documentation below it:
## Covered baselines

| PHP branch | First patched release |
| --- | --- |
| 8.2 | 8.2.35 |
| 8.3 | 8.3.36 |
| 8.4 | 8.4.27 |
| 8.5 | 8.5.12 |

## Result meanings

- `EXPOSED`: The supplied release is below the official patched baseline for its branch.
- `PATCHED BASELINE MET`: The supplied release is at or above the official patched baseline for its branch.
- `REVIEW REQUIRED`: The branch is outside this checker's four-branch matrix.

## Limitations

This checker does not verify vendor backports, package provenance, configuration mitigations, web-server SAPI, application exposure, or whether a supplied value matches the runtime actually deployed. Validate those separately before closing a remediation ticket.

## Official sources

- https://www.php.net/archive/2026.php
- https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w
- https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp

Why Document the Limitations?

A version comparison provides a screening result. Vendor backports and deployed runtime details still require separate validation.

The limitations keep the report useful without overstating what the checker proves.

  • Save README.md.

You should now see sections for covered baselines and result meanings. You should also see limitations and official sources.

Use these tabs to compare the finished documentation with the required README.

✔️ Awesome, I've got everything!

Your README now explains how to run the checker and how to interpret its results responsibly.

ⓧ I'd like to double check the full code

# PHP2Shell Patch Readiness Checker

A defensive PowerShell tool that screens a supplied PHP release against the official patched baselines for CVE-2026-109618 and CVE-2026-109619.

## What it does

- Accepts a dotted PHP release such as `8.5.11`.
- Selects the official patched baseline for PHP 8.2, 8.3, 8.4, or 8.5.
- Returns `EXPOSED`, `PATCHED BASELINE MET`, or `REVIEW REQUIRED`.
- Writes a Markdown assessment with remediation, limitations, and official sources.

## Safe scope

This project performs version-based screening only. It does not scan hosts, send web requests, reproduce memory corruption, or execute exploit code.

## Run an exposed example

```powershell
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.5.11 -ReportPath .\exposed-8.5.11.md
```

Expected status: `EXPOSED`

## Run a patched-baseline example

```powershell
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.5.12 -ReportPath .\patched-8.5.12.md
```

Expected status: `PATCHED BASELINE MET`

## Covered baselines

| PHP branch | First patched release |
| --- | --- |
| 8.2 | 8.2.35 |
| 8.3 | 8.3.36 |
| 8.4 | 8.4.27 |
| 8.5 | 8.5.12 |

## Result meanings

- `EXPOSED`: The supplied release is below the official patched baseline for its branch.
- `PATCHED BASELINE MET`: The supplied release is at or above the official patched baseline for its branch.
- `REVIEW REQUIRED`: The branch is outside this checker's four-branch matrix.

## Limitations

This checker does not verify vendor backports, package provenance, configuration mitigations, web-server SAPI, application exposure, or whether a supplied value matches the runtime actually deployed. Validate those separately before closing a remediation ticket.

## Official sources

- https://www.php.net/archive/2026.php
- https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w
- https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp

How to Compare the README

Confirm that the safe scope appears before the commands. Check the four-row baseline table and the three result meanings.

Finish by comparing the limitations and all three official source links.

The final evidence pair tests both sides of the PHP 8.5 boundary. Before you run the commands, what status do you expect from each supplied release?

  • Generate both evidence reports by running these commands:
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.5.11 -ReportPath .\exposed-8.5.11.md
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\Test-PHP2ShellExposure.ps1 -PhpVersion 8.5.12 -ReportPath .\patched-8.5.12.md

What Do the Results Prove?

The first command prints Status: EXPOSED. The second command prints Status: PATCHED BASELINE MET.

Together they demonstrate the last exposed release and the first patched release for the PHP 8.5 branch.

  • Select exposed-8.5.11.md in the VS Code Explorer sidebar.
  • Confirm its status line reads EXPOSED.
  • Select patched-8.5.12.md in the VS Code Explorer sidebar.
  • Confirm its status line reads PATCHED BASELINE MET.

The repository now contains two timestamped assessments with matching findings and different boundary results.

Publish the public repository

Publishing makes the checker and its evidence available for review. Use the signed-in path you established earlier.

GitHub Desktop

Use GitHub Desktop when the local repository is already connected to your account.

  • Switch back to GitHub Desktop from earlier.
  • Review the script and Markdown files waiting to be committed.
  • Enter Generate evidence and documentation as the commit message.
  • Commit the project files.
  • Select Publish repository.
  • Leave Keep this code private unselected.
  • Select Publish repository to make the repository public.

GitHub Browser Fallback

Use the browser fallback when GitHub Desktop is unavailable on your Windows computer.

  • Return to the php2shell-patch-readiness-checker repository page from earlier.
  • Select Add file.
  • Click Upload files.
  • Choose Test-PHP2ShellExposure.ps1 from the local repository.
  • Choose README.md from the local repository.
  • Choose both generated Markdown reports from the local repository.
  • Enter Generate evidence and documentation as the commit message.
  • Select Propose changes.

Before you verify the publication, which four project files should appear in the repository?

  • Select Repository in GitHub Desktop if you used the desktop path.
  • Select View on GitHub.
  • Return to the repository page if you used the browser fallback.

Your browser should show the public php2shell-patch-readiness-checker repository. The README should render beneath the file list.

The file list should include Test-PHP2ShellExposure.ps1, README.md, exposed-8.5.11.md, and patched-8.5.12.md.

Repository Missing a File?

Return to your chosen publishing path. Repeat the upload or commit process with the missing file.

Ask for help checking the publishing state: Why is a local project file missing from my published GitHub repository?

Strong finish. Your checker now produces durable security evidence with explicit scope limits. The public repository gives reviewers a reproducible defensive project they can inspect and run.

Secret mission

Automate the Boundary Tests

Manual spot checks can miss a broken branch boundary. Build an automated test matrix that checks the last affected release and first patched release across all four covered PHP branches.

Clean Up Your Resources

Clean Up Your Resources

Your project uses GitHub Free at $0 USD per month. Keeping either project copy creates no project-related charges.

Resources you used:

  • Public GitHub repository named php2shell-patch-readiness-checker containing your checker files plus evidence reports.
  • Local repository folder used in Visual Studio Code. This includes your checker scripts plus Markdown evidence reports.

Keep everything running

No action is needed. Choose this if you want both project copies available for future updates.

  • Keep the public repository available on GitHub.
  • Keep the local repository folder available on your computer.

Pause - I'll come back to this later

Pause keeps both copies intact without deleting your evidence. The checker has no background process to stop.

  • Sign out of GitHub Desktop if you used desktop publishing.
  • Leave the GitHub repository unchanged.
  • Leave the local repository folder unchanged.

Delete - I don't want to use this again

This option removes your published repository. It also removes your local working copy.

  • Copy every file you want to keep to a folder outside your local repository.

Keep a Backup First

Permanent deletion can feel risky. The backup you just made preserves the files you still need.

  • Open the php2shell-patch-readiness-checker repository on GitHub.
  • Select Settings.
  • Scroll to Danger Zone.
  • Choose Delete this repository.
  • Read the deletion warnings.
  • Confirm the warnings.
  • Type php2shell-patch-readiness-checker when GitHub asks for the repository name.
  • Complete the deletion.

The Local Copy Remains

GitHub repository deletion leaves the local folder on your computer. The next actions remove this separate copy.

  • Close Visual Studio Code.
  • Use your computer's folder browser to locate the local repository folder that was open in Visual Studio Code.
  • Delete the local repository folder.
  • Confirm the folder is no longer listed.

Cleanup complete. Both project copies are now removed.

Nice Work!

Nice Work!

You did it! Your defensive Windows PowerShell checker now translates two critical PHP advisories into branch-aware patch classifications. Your public GitHub repository preserves the complete project as reviewable security evidence.

You've learned how to:

  • Diagnose the one-baseline mistake by reproducing a false EXPOSED result before rebuilding the classifier with structured version parsing.
  • Generate two Markdown evidence reports that preserve both sides of the patch boundary.
  • Publish a public defensive-security repository that another reviewer can inspect, run, or verify without exploit code.
  • Secret Mission: Automate an eight-case boundary test matrix covering the last affected release plus the first patched release in every supported branch.

Ready to quiz yourself?