Build a PHP2Shell Readiness Dashboard

Build a local PHP dashboard that assesses patch readiness for critical CVEs.

Introduction

30 Second Summary

A critical software alert can leave you staring at version numbers with no clear answer about whether your installation needs an urgent update. A useful check turns those details into a decision you can explain.

In this project, you will build a local PHP dashboard that assesses whether your runtime meets the official PHP2Shell patch baseline. You will publish a documented GitHub repository that proves the result without exposing a live diagnostic page.

What You'll Build

You’ll open a dark localhost page where your PHP runtime becomes a clear green Patched verdict supported by official sources plus configuration evidence.

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

  • A visual runtime inventory that displays PHP 8.5.12 plus the cli-server Server API on localhost.
  • An evidence-backed patch verdict that turns the running version into Patched, At risk, or Not covered using the official fixed releases.
  • A publishable defensive evidence package that explains CVE-2026-109618 plus CVE-2026-109619. It also shows five configuration checks beside the update-first warning.
  • Secret Mission: Add a safe version dropdown that switches between allowlisted synthetic PHP 8.5.11 plus PHP 8.5.12 profiles without installing an affected runtime.

Are there any prerequisites?

You only need a Windows device. The guide includes setup for every required tool plus your free account.

Before We Start

This first checkpoint locks in the safety boundary before any hands-on work begins. You are building a defensive localhost-only PHP patch-readiness dashboard without exploit code or vulnerable software.

Set Up a Patched Windows PHP Workspace

A patch-readiness dashboard needs a trusted runtime. Starting with an affected PHP release would create a containment problem before you assess anything.

You'll prepare GitHub Desktop 3.6.7 with a local repository. You'll also install Visual Studio Code, the Microsoft Visual C++ v14 Redistributable, and patched PHP 8.5.12.

In this step, get ready to:
  • Create a verified GitHub workspace with an initial local repository.
  • Install the Windows editor plus the runtime required by PHP.
  • Verify the patched PHP release against its official checksum.
Create your GitHub account and local repository

A Git repository records the files that make up your project. GitHub Desktop gives you a visual workflow for creating that repository.

  • In Microsoft Edge, visit the GitHub account signup page.
  • Complete the signup form with your personal account details.
  • Open the verification message from GitHub in your email inbox.
  • Follow the verification link in that message.

Your GitHub account page should now show your signed-in account without an email verification warning.

GitHub Desktop requires Windows 10 64-bit or later. Choose the tab that matches the version currently available on your device.

✔️ I have version 3.6.7

  • Press the Windows key to open Windows search.
  • Type GitHub Desktop into Windows search.
  • Press Enter to open GitHub Desktop.
  • Select Help from the menu bar.
  • Select About GitHub Desktop.

The version shown in the window should be 3.6.7.

ⓧ I have an older version

GitHub Desktop can update an existing installation without removing your saved preferences or repositories.

The installer updates the application. GitHub Desktop launches when the installation finishes.

  • Select Help from the GitHub Desktop menu bar.
  • Select About GitHub Desktop.

The version shown in the window should now be 3.6.7.

ⓧ GitHub Desktop is not installed

The Windows installer launches GitHub Desktop when setup is complete. The first launch can take a moment while the application prepares its files.

GitHub Desktop should launch after the installation completes.

  • Select Help from the GitHub Desktop menu bar.
  • Select About GitHub Desktop.

The version shown in the window should be 3.6.7.

The next action authorizes GitHub Desktop to use your verified account. Browser authentication keeps your account password out of the desktop application.

  • In GitHub Desktop, choose the sign-in option for GitHub.com.
  • Complete the authorization flow in Microsoft Edge.
  • Return to GitHub Desktop after the browser confirms authorization.
  • Choose your verified account name if GitHub Desktop requests a commit identity.
  • Choose your verified email address if GitHub Desktop requests a commit email.

GitHub Desktop should now show your signed-in account. You are ready to create the local project history.

  • In GitHub Desktop, select Create a New Repository on your Hard Drive....
  • Enter php2shell-readiness-dashboard in the repository name field.
  • Select the option that initializes the repository with a README.
  • Use the create control to initialize the local repository.

That is your first project asset in place. GitHub Desktop should show php2shell-readiness-dashboard as the current repository.

  • Open the repository's local folder from GitHub Desktop.

File Explorer should show README.md inside the php2shell-readiness-dashboard folder. GitHub Desktop has also initialized the hidden .git repository data.

Having trouble with GitHub Desktop?

Confirm your device runs 64-bit Windows 10 or later. An unsupported Windows version cannot run this GitHub Desktop release.

If browser authorization does not return to the application, keep GitHub Desktop open before repeating the sign-in flow.

Help me fix my GitHub Desktop installation or sign-in flow.

Install the Windows editor and runtime dependency

Visual Studio Code is where you will edit the dashboard files. The User setup keeps the editor within your Windows account.

Microsoft Edge should show the Visual Studio Code installer in your completed downloads.

  • Run the Visual Studio Code User setup installer from your Downloads folder.

You'll see the Visual Studio Code setup window with the installation prompts.

  • Complete the setup prompts using the default installation options.
  • Press the Windows key to open Windows search.
  • Type Visual Studio Code into Windows search.
  • Press Enter to open Visual Studio Code.

Your editor is ready when the Visual Studio Code welcome screen opens.

Why install the Visual C++ runtime?

The PHP VS17 x64 build depends on the matching Microsoft Visual C++ runtime. Installing the x64 redistributable gives the PHP executable the Windows libraries it needs to start.

This installer changes shared Windows runtime files. It uses Microsoft's supported package for 64-bit applications.

Microsoft Edge should show vc_redist.x64.exe in your completed downloads.

  • Run vc_redist.x64.exe from your Downloads folder.

Windows may request permission to let the Microsoft installer make changes. Approving that request opens the redistributable setup window.

  • Approve the Windows permission request.
  • Complete the Microsoft Visual C++ Redistributable setup.

You should see a setup completion confirmation for the x64 runtime.

Installer not completing?

Close applications that are waiting for a Windows restart. Restart Windows before running vc_redist.x64.exe again.

If the installer reports an existing installation, use its available setup option to confirm the x64 runtime is present.

Help me troubleshoot the Visual C++ runtime setup.

Install and verify patched PHP

The official Windows PHP page provides several builds. The VS17 x64 Non Thread Safe Zip fits this command-line localhost project.

A SHA-256 hash fingerprints the downloaded archive. Matching the official value confirms that the file has the expected contents before you run it.

You should see a VS17 x64 Non Thread Safe row with a Zip download.

  • Select the Zip download in the VS17 x64 Non Thread Safe row.
  • Save the PHP archive in your Downloads folder.

Microsoft Edge should show the PHP Zip in your completed downloads.

  • Record the archive's full path here: path to the downloaded PHP Zip.
  • Press the Windows key to open Windows search.
  • Type Windows PowerShell into Windows search.
  • Press Enter to open Windows PowerShell.
  • Calculate the archive's SHA-256 value by running:
Get-FileHash -LiteralPath '[[PHP_ZIP_PATH="path to the downloaded PHP Zip"]]' -Algorithm SHA256

What does this command do?

  • Get-FileHash calculates a fingerprint from the archive's contents.
  • -LiteralPath uses the exact path you recorded.
  • -Algorithm selects SHA256 for the integrity check.

PowerShell displays a 64-character hash. It should match 2ab242af38d76bdf417cde5ec048b1bcd76a5a40f39a3d15d5f046f38571c477.

That match confirms the PHP archive has the official contents. You can now extract the executable with confidence.

Does the hash differ?

Do not extract an archive with a different hash. Remove that download before getting a fresh Zip from the official PHP Windows page.

Help me investigate a PHP archive checksum mismatch.

  • In File Explorer, locate the verified PHP Zip in your Downloads folder.
  • Choose File Explorer's archive extraction action.

You'll see an extraction dialog that asks where the archive contents should go.

  • Enter C:\php as the extraction destination.
  • Complete the archive extraction.

File Explorer should now show php.exe directly inside C:\php.

  • Press the Windows key to open a fresh Windows search.
  • Type Windows PowerShell into Windows search.
  • Press Enter to open a fresh PowerShell terminal.

Before you check, what version do you expect the newly extracted executable to report?

  • Verify the patched PHP executable by running:
C:\php\php.exe -v

What does this command do?

The -v option asks the PHP command-line executable to print its SAPI name plus its version. Using the full path proves you are checking the executable inside C:\php.

Choose the result that matches the first line in PowerShell.

✔️ I see PHP 8.5.12 (cli)

The first line begins with PHP 8.5.12 (cli).

Your patched runtime is installed and taking commands. This is the safe foundation for the localhost dashboard.

ⓧ I see an older version

Replacing this standalone PHP folder removes only the copy stored in C:\php. Your verified Zip remains available for a clean extraction.

  • Remove the existing C:\php folder using File Explorer.
  • Extract the verified PHP 8.5.12 Zip to C:\php again.
  • Open a fresh Windows PowerShell terminal through Windows search.
  • Repeat the version check by running:
C:\php\php.exe -v

What does this check prove?

The full executable path bypasses any older PHP copy available elsewhere on Windows. The first line should now begin with PHP 8.5.12 (cli).

ⓧ The command cannot run

This result usually means the Zip contents are nested inside another folder. A missing Visual C++ runtime can also stop the executable from starting.

  • Confirm php.exe exists directly inside C:\php.
  • Move the extracted PHP contents directly into C:\php if they are inside a nested folder.
  • Run vc_redist.x64.exe again if Windows mentions a missing runtime component.
  • Open a fresh Windows PowerShell terminal through Windows search.
  • Repeat the version check by running:
C:\php\php.exe -v

What does this check prove?

A successful response proves Windows can start the PHP executable with its required runtime. The first line should begin with PHP 8.5.12 (cli).

PHP still not starting?

Confirm that you downloaded the VS17 x64 Non Thread Safe Zip. An x86 build does not match this x64 setup.

Help me diagnose why the PHP executable will not start.

✔️ Awesome, I've got everything!

Your verified account is ready. Your local repository plus the patched PHP workspace are also ready.

ⓧ I'd like to double check the full code

Compare your workspace with this exact setup checkpoint:

  • C:\php\php.exe: PHP 8.5.12 VS17 x64 Non Thread Safe executable installed and verified. SHA-256 verified as 2ab242af38d76bdf417cde5ec048b1bcd76a5a40f39a3d15d5f046f38571c477.
  • php2shell-readiness-dashboard/.git: Local Git repository initialized by GitHub Desktop and associated with the signed-in GitHub account.
  • php2shell-readiness-dashboard/README.md: Initial GitHub Desktop README created for the local repository php2shell-readiness-dashboard.

Your Windows workspace now starts from a patched runtime. Next, you'll turn that runtime information into the first visible localhost dashboard.

Show the Runtime in a Local Dashboard

The patched PHP runtime is ready on your Windows device. You can now turn its terminal output into something you can demonstrate in a browser.

A raw version confirms that PHP is running. It does not explain whether that release meets the official patch baseline.

In this step, you'll display PHP_VERSION and PHP_SAPI on a localhost dashboard. The missing verdict exposes the assessment gap that remains.

In this step, get ready to:
  • Create index.php with the project heading and runtime cards.
  • Add inline styling for a dark responsive dashboard.
  • Serve the dashboard on localhost for a browser check.
Create the runtime page

PHP provides runtime constants that describe the process executing a page. PHP_VERSION supplies the release number. PHP_SAPI identifies the interface serving the request.

  • Press the Windows key to open Windows search.
  • Type Visual Studio Code into the search field.
  • Press Enter to open Visual Studio Code.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the existing php2shell-readiness-dashboard repository folder.
  • Click Select Folder.

You'll see README.md in the Explorer sidebar. This confirms that VS Code has the correct repository open.

  • Click the new file icon beside php2shell-readiness-dashboard in the Explorer sidebar.
  • Enter index.php as the file name.
  • Build the page structure and runtime cards by pasting this code into index.php:
<?php

?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PHP2Shell Patch Readiness Dashboard</title>
<style>
</style>
</head>
<body>
<main class="container">
<header class="hero">
<p class="eyebrow">Defensive PHP security portfolio project</p>
<h1>PHP2Shell Patch Readiness Dashboard</h1>
<p class="subtitle">Local evidence for CVE-2026-109618 and CVE-2026-109619. No exploit payloads and no external targets.</p>
</header>

<section>
<p class="label">Runtime inventory</p><h2>What is actually running?</h2>
<div class="grid">
<article class="card"><h3>PHP version</h3><p class="value"><?= PHP_VERSION ?></p></article>
<article class="card"><h3>Server API</h3><p class="value"><?= PHP_SAPI ?></p></article>
</div>
</section>
</main>
</body>
</html>

What does this code do?

  • The header states the defensive boundary of the project. It makes clear that the page uses local evidence only.
  • The first card reads PHP_VERSION from the running process. This value identifies the installed PHP release.
  • The second card reads PHP_SAPI from the same process. This value identifies how PHP is serving the page.
  • Save index.php by pressing Ctrl+S.
  • Confirm that index.php remains listed beside README.md in the Explorer sidebar.

Can't find index.php?

Check that the Explorer sidebar shows the php2shell-readiness-dashboard folder. Create index.php beside README.md if it was saved elsewhere.

Make sure the file name ends with .php.

Help me check where my PHP file was created.

Style and finalize the page

The page now has its complete content structure. CSS turns that structure into a responsive dashboard with clear visual groupings.

  • In index.php, find the PHP block at the top of the file.
  • Use this snippet to confirm that you found the correct block:
<?php

?>

Why start at the top?

This block runs before the browser receives any HTML. It is the correct location for file-wide PHP declarations.

  • Insert the strict type declaration so the top of index.php looks like this:
<?php

declare(strict_types=1);
?>

Why use strict types?

The strict_types declaration sets a clear type-handling rule for PHP code in this file. Later functions can rely on that rule as the dashboard grows.

  • Find the empty style block below the page title.
  • Use this snippet to confirm that you found the insertion point:
<style>
</style>

What is this block for?

The style block keeps this small dashboard in one file. The browser applies its rules to the HTML below.

  • Replace the empty style block with this completed block:
<style>
:root{color-scheme:dark;font-family:Segoe UI,sans-serif;background:#07111f;color:#e5eefb}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at top right,#12304a,#07111f 42%);min-height:100vh}a{color:#7dd3fc}.container{width:min(1080px,calc(100% - 32px));margin:auto;padding:48px 0}.hero{max-width:820px}.eyebrow,.label{color:#38bdf8;font-size:.78rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.subtitle,.card p{color:#afc0d8;line-height:1.6}h1{font-size:clamp(2.2rem,6vw,4.2rem);line-height:1;margin:.3em 0}section{margin-top:32px}.card{border:1px solid #263a55;border-radius:16px;background:#0d1a2de8;padding:22px}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.value{font:800 1.3rem Consolas,monospace;color:#f8fafc!important}@media(max-width:720px){.grid{grid-template-columns:1fr}}
</style>

What do these styles change?

  • The page uses a dark background with light text. This creates contrast for the runtime evidence.
  • The grid places the runtime cards side by side on wide screens. The media rule stacks them on narrow screens.
  • The card borders and spacing separate each runtime value. The monospace value style makes the identifiers easy to scan.
  • Save index.php by pressing Ctrl+S.
  • Confirm that the unsaved-change indicator disappears from the index.php tab.

Styles missing from the file?

Check that the CSS line sits between the opening and closing style tags. Keep both tags inside the page's head section.

Help me check the style block in index.php.

✔️ Awesome, I've got everything!

Great. Double check that index.php is saved before starting the local server.

ⓧ I'd like to double check the full code

Compare your complete index.php file with this reference:

<?php

declare(strict_types=1);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PHP2Shell Patch Readiness Dashboard</title>
<style>
:root{color-scheme:dark;font-family:Segoe UI,sans-serif;background:#07111f;color:#e5eefb}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at top right,#12304a,#07111f 42%);min-height:100vh}a{color:#7dd3fc}.container{width:min(1080px,calc(100% - 32px));margin:auto;padding:48px 0}.hero{max-width:820px}.eyebrow,.label{color:#38bdf8;font-size:.78rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.subtitle,.card p{color:#afc0d8;line-height:1.6}h1{font-size:clamp(2.2rem,6vw,4.2rem);line-height:1;margin:.3em 0}section{margin-top:32px}.card{border:1px solid #263a55;border-radius:16px;background:#0d1a2de8;padding:22px}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.value{font:800 1.3rem Consolas,monospace;color:#f8fafc!important}@media(max-width:720px){.grid{grid-template-columns:1fr}}
</style>
</head>
<body>
<main class="container">
<header class="hero">
<p class="eyebrow">Defensive PHP security portfolio project</p>
<h1>PHP2Shell Patch Readiness Dashboard</h1>
<p class="subtitle">Local evidence for CVE-2026-109618 and CVE-2026-109619. No exploit payloads and no external targets.</p>
</header>

<section>
<p class="label">Runtime inventory</p><h2>What is actually running?</h2>
<div class="grid">
<article class="card"><h3>PHP version</h3><p class="value"><?= PHP_VERSION ?></p></article>
<article class="card"><h3>Server API</h3><p class="value"><?= PHP_SAPI ?></p></article>
</div>
</section>
</main>
</body>
</html>

What should match?

Your file should contain one strict type declaration and one inline style block. The body should contain only the project header and two runtime cards.

Start the local server and inspect the gap

PHP includes a development server for controlled local demonstrations. It serves files from the folder where its command runs.

  • Click Terminal in the VS Code top menu.
  • Click New Terminal.

You'll see a PowerShell prompt for the open php2shell-readiness-dashboard repository. The server uses this folder as its document directory.

Before you start it, do you expect a patch verdict to appear with only two runtime identifiers?

  • Start the local PHP server by running this command:
C:\php\php.exe -S localhost:8000

What does this command do?

The command starts the PHP development server on localhost port 8000. It serves index.php from the repository folder.

The terminal remains occupied while the server runs. Pressing Ctrl+C stops it when you finish later. Keep it running for this project.

  • Press the Windows key to open Windows search.
  • Type Microsoft Edge into the search field.
  • Press Enter to open Microsoft Edge.
  • Click the browser address bar.
  • Enter http://localhost:8000.
  • Press Enter to load the dashboard.

You'll see 8.5.12 in the PHP version card. You'll see cli-server in the Server API card.

No patch verdict appears. That visible gap is intentional because the page only reports the raw runtime identifiers.

Why is the missing verdict useful?

The two cards prove which PHP release is serving the page. They also prove that inventory data alone cannot judge whether the release meets a security baseline.

That's your first visible dashboard running locally. You now have a safe browser view of the exact PHP process behind it.

Dashboard not loading?

  • Keep the VS Code terminal running while Microsoft Edge loads the page.
  • Check that the terminal belongs to the open php2shell-readiness-dashboard repository.
  • Compare your index.php file with the full-code tab if either runtime card is missing.

Help me debug why my localhost PHP dashboard is not loading.

Your dashboard now proves which runtime is serving localhost. Next, you'll compare that release with the official fixed-version baseline to produce a patch verdict.

Turn the Version into a Patch Verdict

Your local PHP dashboard now identifies the runtime on localhost. A raw version gives you data without answering the patch question.

This step connects each supported release line to its official fixed version. The dashboard can then turn the running release into a clear verdict.

In this step, get ready to:
  • Map each supported PHP release line to its fixed version.
  • Compare the running version with the required baseline.
  • Display the result in a patch verdict card.
Map the running release line

A fixed release applies to one PHP maintenance line. Your dashboard needs the correct baseline before it can assess the running version.

Before you refresh, do you think the current page can tell you whether its runtime meets that baseline?

  • Switch back to Microsoft Edge.
  • Refresh http://localhost:8000.

You should see the runtime version and Server API cards. You should see no patch verdict.

Why the first dashboard falls short

The runtime card reports the version that is executing. It does not compare that version with the fixed release for its maintenance line.

The missing verdict is the assessment gap you are closing now.

  • Switch back to index.php in Visual Studio Code.
  • Find declare(strict_types=1); near the top of the file.
  • Place your cursor on the blank line below the declaration.
  • Add the fixed-version matrix and runtime inputs by pasting this code:
function escapeHtml(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

$fixedVersions = [
    '8.2' => '8.2.35',
    '8.3' => '8.3.36',
    '8.4' => '8.4.27',
    '8.5' => '8.5.12',
];

$assessedVersion = PHP_VERSION;
$releaseLine = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;

What does this code do?

  • The escapeHtml() helper prepares dynamic values for safe HTML output.
  • The $fixedVersions matrix stores the fixed baseline for each supported release line.
  • The $assessedVersion variable stores the complete running version from PHP_VERSION.
  • The PHP_MAJOR_VERSION constant supplies the major version number.
  • The PHP_MINOR_VERSION constant supplies the minor version number used in $releaseLine.
  • Save index.php by pressing Ctrl+S.
  • Switch back to Microsoft Edge.
  • Refresh the local dashboard.

You should see the PHP version card showing 8.5.12. The Server API card should show cli-server.

Page stopped loading?

Confirm that the new PHP code sits above the closing ?> tag. Compare each quote with the code block.

Check that the PowerShell server from the previous step is still running.

Ask for help with the new PHP block: Help me find the syntax problem in my version matrix.

Calculate the patch status

The release line selects the relevant baseline from the matrix. The version_compare() function checks whether the assessed version meets it.

  • Find the $releaseLine = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; assignment in index.php.
  • Place your cursor on the blank line below that assignment.
  • Add the verdict logic by pasting this code:
if (!isset($fixedVersions[$releaseLine])) {
    $status = 'Not covered';
    $statusClass = 'unknown';
    $requiredVersion = 'Check current guidance';
    $message = 'This release line is outside the October 2026 advisory matrix.';
} elseif (version_compare($assessedVersion, $fixedVersions[$releaseLine], '>=')) {
    $status = 'Patched';
    $statusClass = 'patched';
    $requiredVersion = $fixedVersions[$releaseLine];
    $message = 'This runtime meets the fixed-version baseline for both CVEs.';
} else {
    $status = 'At risk';
    $statusClass = 'risk';
    $requiredVersion = $fixedVersions[$releaseLine];
    $message = 'This runtime is below the fixed release. Update immediately.';
}

How is the verdict chosen?

  • The first branch assigns Not covered when the release line is absent from the matrix.
  • The second branch uses >= to assign Patched when the runtime meets the fixed baseline.
  • The final branch assigns At risk when the runtime is below the required version.
  • The $statusClass value stores the visual state for the verdict card.
  • Save index.php.
  • Switch back to Microsoft Edge.
  • Refresh the dashboard.

You should see both runtime cards load normally. A successful reload proves that PHP evaluated the comparison logic.

Dashboard not loading?

Confirm that each condition has a matching closing brace. Check that the comparison block remains above the closing PHP tag.

Compare every occurrence of $fixedVersions with the matrix name. Compare every occurrence of $releaseLine with the assignment name.

Ask for help with the condition: Help me debug the patch-status logic.

Display the patch verdict

The status variables now hold the assessment result. A dedicated section makes the verdict visible beside the assessed release details.

  • Find the closing </header> tag in index.php.
  • Place your cursor on the blank line below that tag.
  • Add the patch verdict section by pasting this code:
<section class="status <?= escapeHtml($statusClass) ?>">
<div><p class="label">Patch verdict</p><h2><?= escapeHtml($status) ?></h2><p><?= escapeHtml($message) ?></p></div>
<dl class="details">
<div><dt>Assessed</dt><dd><?= escapeHtml($assessedVersion) ?></dd></div>
<div><dt>Release line</dt><dd><?= escapeHtml($releaseLine) ?></dd></div>
<div><dt>Required</dt><dd><?= escapeHtml($requiredVersion) ?></dd></div>
</dl>
</section>

What does the status section show?

  • The section class includes $statusClass so the verdict controls its visual state.
  • The heading displays the calculated $status value.
  • The details list displays the assessed version.
  • The details list displays the release line.
  • The details list displays the required fixed version.
  • Every dynamic status value passes through escapeHtml() before entering the page.
  • Save index.php.

Before you refresh, do you expect PHP 8.5.12 to meet the required baseline?

  • Switch back to Microsoft Edge.
  • Refresh http://localhost:8000.

You should see a patch verdict section above the runtime inventory. It should show Patched with assessed version 8.5.12.

The release line should show 8.5. The required version should show 8.5.12.

Verdict section missing?

Confirm that the status section sits below </header>. Check that it remains above the runtime inventory section.

Compare every PHP opening tag with the code block. A missing question mark can stop the dynamic values from rendering.

Ask for help with the output section: Help me find why the verdict section is missing.

The verdict is functional. The next change gives each outcome a clear border colour and keeps the details readable on smaller screens.

  • Select everything between the existing <style> tags in index.php.
  • Replace the selected stylesheet with this code:
:root{color-scheme:dark;font-family:Segoe UI,sans-serif;background:#07111f;color:#e5eefb}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at top right,#12304a,#07111f 42%);min-height:100vh}a{color:#7dd3fc}.container{width:min(1080px,calc(100% - 32px));margin:auto;padding:48px 0}.hero{max-width:820px}.eyebrow,.label{color:#38bdf8;font-size:.78rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.subtitle,.card p,.status p{color:#afc0d8;line-height:1.6}h1{font-size:clamp(2.2rem,6vw,4.2rem);line-height:1;margin:.3em 0}section{margin-top:32px}.status,.card{border:1px solid #263a55;border-radius:16px;background:#0d1a2de8}.status{display:grid;grid-template-columns:1.3fr 1fr;gap:24px;padding:24px;border-left-width:6px}.status.patched{border-left-color:#22c55e}.status.risk{border-left-color:#f43f5e}.status.unknown{border-left-color:#f59e0b}.details{margin:0}.details div{display:flex;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid #263a55}.details dd{margin:0;font-weight:800}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.card{padding:22px}.value{font:800 1.3rem Consolas,monospace;color:#f8fafc!important}@media(max-width:720px){.status,.grid{grid-template-columns:1fr}}

What do these styles add?

  • The .status grid places the verdict beside its release details.
  • The .patched class gives a patched result a green border.
  • The .risk class gives an at-risk result a red border.
  • The .unknown class gives an uncovered release line an amber border.
  • The responsive rule stacks the verdict content on narrow screens.
  • Save index.php.

Before you refresh, which border colour do you expect the Patched verdict to use?

  • Switch back to Microsoft Edge.
  • Refresh the local dashboard.

You should see a green-bordered Patched card. It should show assessed version 8.5.12.

You should also see release line 8.5 and required version 8.5.12.

That is the comparison loop working. Your dashboard now translates its running PHP version into a patch decision.

Verdict missing or styled incorrectly?

Confirm that the replacement stylesheet remains inside the existing <style> tags. Check that the entire CSS block was copied.

If the result is not Patched, confirm that the existing PowerShell server is running PHP 8.5.12.

Ask for help with the final result: Help me debug the patch verdict.

✔️ Awesome, I've got everything!

Your assessment logic and styled patch verdict are now saved in index.php.

ⓧ I'd like to double check the full code

Compare your complete index.php file with this version:

<?php

declare(strict_types=1);

function escapeHtml(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

$fixedVersions = [
    '8.2' => '8.2.35',
    '8.3' => '8.3.36',
    '8.4' => '8.4.27',
    '8.5' => '8.5.12',
];

$assessedVersion = PHP_VERSION;
$releaseLine = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;

if (!isset($fixedVersions[$releaseLine])) {
    $status = 'Not covered';
    $statusClass = 'unknown';
    $requiredVersion = 'Check current guidance';
    $message = 'This release line is outside the October 2026 advisory matrix.';
} elseif (version_compare($assessedVersion, $fixedVersions[$releaseLine], '>=')) {
    $status = 'Patched';
    $statusClass = 'patched';
    $requiredVersion = $fixedVersions[$releaseLine];
    $message = 'This runtime meets the fixed-version baseline for both CVEs.';
} else {
    $status = 'At risk';
    $statusClass = 'risk';
    $requiredVersion = $fixedVersions[$releaseLine];
    $message = 'This runtime is below the fixed release. Update immediately.';
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PHP2Shell Patch Readiness Dashboard</title>
<style>
:root{color-scheme:dark;font-family:Segoe UI,sans-serif;background:#07111f;color:#e5eefb}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at top right,#12304a,#07111f 42%);min-height:100vh}a{color:#7dd3fc}.container{width:min(1080px,calc(100% - 32px));margin:auto;padding:48px 0}.hero{max-width:820px}.eyebrow,.label{color:#38bdf8;font-size:.78rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.subtitle,.card p,.status p{color:#afc0d8;line-height:1.6}h1{font-size:clamp(2.2rem,6vw,4.2rem);line-height:1;margin:.3em 0}section{margin-top:32px}.status,.card{border:1px solid #263a55;border-radius:16px;background:#0d1a2de8}.status{display:grid;grid-template-columns:1.3fr 1fr;gap:24px;padding:24px;border-left-width:6px}.status.patched{border-left-color:#22c55e}.status.risk{border-left-color:#f43f5e}.status.unknown{border-left-color:#f59e0b}.details{margin:0}.details div{display:flex;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid #263a55}.details dd{margin:0;font-weight:800}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.card{padding:22px}.value{font:800 1.3rem Consolas,monospace;color:#f8fafc!important}@media(max-width:720px){.status,.grid{grid-template-columns:1fr}}
</style>
</head>
<body>
<main class="container">
<header class="hero">
<p class="eyebrow">Defensive PHP security portfolio project</p>
<h1>PHP2Shell Patch Readiness Dashboard</h1>
<p class="subtitle">Local evidence for CVE-2026-109618 and CVE-2026-109619. No exploit payloads and no external targets.</p>
</header>

<section class="status <?= escapeHtml($statusClass) ?>">
<div><p class="label">Patch verdict</p><h2><?= escapeHtml($status) ?></h2><p><?= escapeHtml($message) ?></p></div>
<dl class="details">
<div><dt>Assessed</dt><dd><?= escapeHtml($assessedVersion) ?></dd></div>
<div><dt>Release line</dt><dd><?= escapeHtml($releaseLine) ?></dd></div>
<div><dt>Required</dt><dd><?= escapeHtml($requiredVersion) ?></dd></div>
</dl>
</section>

<section>
<p class="label">Runtime inventory</p><h2>What is actually running?</h2>
<div class="grid">
<article class="card"><h3>PHP version</h3><p class="value"><?= PHP_VERSION ?></p></article>
<article class="card"><h3>Server API</h3><p class="value"><?= PHP_SAPI ?></p></article>
</div>
</section>
</main>
</body>
</html>

Your dashboard can now judge its running release against the official patch baseline. Next, you will add the evidence that explains why this verdict matters.

Add CVE Context and Configuration Evidence

Your PHP dashboard already compares the local release with the official fixed-version baseline. That verdict tells you whether the runtime meets the required release.

A verdict without supporting evidence is hard to trust. An assessor needs to see which vulnerabilities the verdict covers.

You will connect the result to the two official CVE advisories. You will also record the configuration values that support an update-first assessment.

In this step, get ready to:
  • Add the two official vulnerability records.
  • Collect five configuration values.
  • Verify every dynamic value is escaped before display.
Add the official vulnerability records

The patch verdict covers two critical vulnerabilities with the same fixed releases. A structured array keeps each identifier beside its summary and official advisory URL.

  • In index.php, find the final closing brace below the message This runtime is below the fixed release. Update immediately..
  • Add the vulnerability records immediately after that closing brace:
$vulnerabilities = [
    [
        'id' => 'CVE-2026-109618',
        'ghsa' => 'GHSA-w8hw-w9fx-j79w',
        'title' => 'RCE via OOM corruption',
        'summary' => 'Out-of-memory handling can leave request data invalid, and cleanup may later treat that invalid memory as valid.',
        'url' => 'https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w',
    ],
    [
        'id' => 'CVE-2026-109619',
        'ghsa' => 'GHSA-6wmf-4rv5-p3rp',
        'title' => 'RCE via stale global state',
        'summary' => 'Request-owned state can remain after its memory is freed and be consumed by a later request on the same worker.',
        'url' => 'https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp',
    ],
];

What do these records hold?

  • Each id value identifies one vulnerability covered by the patch verdict.
  • Each ghsa value identifies the matching official security advisory.
  • The summary values explain the memory-handling risk without including exploit instructions.
  • The url values give readers a primary source for each finding.
  • Save index.php.
  • Create a second integrated PowerShell terminal inside the open Visual Studio Code repository.
  • Check the new array by running this command:
C:\php\php.exe -l index.php

What does this check do?

The -l option asks PHP to parse index.php without running the dashboard. This catches malformed arrays before they reach the browser.

You should see No syntax errors detected in index.php in the terminal.

Seeing a syntax error?

Check that every vulnerability record ends with a comma. Confirm that the array ends with ];.

Make sure the summaries use matching single quotation marks.

Help me fix the vulnerability array syntax.

The records now exist in PHP memory. A loop can turn them into consistent cards without duplicating the card markup.

  • In index.php, find the section containing Runtime inventory.
  • Add the following section after the closing </section> for the runtime inventory:
<section>
<p class="label">Official context</p><h2>Why the fixed releases matter</h2>
<div class="grid">
<?php foreach ($vulnerabilities as $vulnerability): ?>
<article class="card">
<div class="meta"><span><?= escapeHtml($vulnerability['id']) ?></span><span>CVSS 9.2 Critical</span></div>
<h3><?= escapeHtml($vulnerability['title']) ?></h3>
<p><?= escapeHtml($vulnerability['summary']) ?></p>
<a href="<?= escapeHtml($vulnerability['url']) ?>" target="_blank" rel="noreferrer">Read <?= escapeHtml($vulnerability['ghsa']) ?></a>
</article>
<?php endforeach; ?>
</div>
</section>

How do the cards work?

  • The foreach loop creates one card for each vulnerability record.
  • The metadata row shows the CVE identifier beside its CVSS 9.2 Critical severity.
  • Each dynamic value passes through escapeHtml() before it enters the page.
  • The advisory link opens the record stored in that card's url value.
  • Save index.php.
  • Return to Microsoft Edge.
  • Refresh http://localhost:8000.

You should see two cards below the runtime inventory. One card shows CVE-2026-109618. The other shows CVE-2026-109619.

CVE cards missing?

Confirm that the card section sits below the runtime inventory section. Check that the loop uses $vulnerabilities with the same spelling as the array.

If the page is blank, run the syntax check again. A missing PHP closing marker inside the loop can stop the page from rendering.

Help me debug the missing CVE cards.

Collect configuration evidence

Configuration values help an assessor describe the current environment. They support triage while the patched runtime remains the complete mitigation.

PHP can return a setting as text or report that the setting does not exist. A helper converts those outcomes into readable dashboard values.

  • In index.php, find the line where $fixedVersions begins.
  • Add the configuration helper immediately above that line:
function readSetting(string $name): string
{
    $value = ini_get($name);
    if ($value === false) {
        return 'Not available';
    }
    return $value === '' ? 'Off or empty' : $value;
}

What does this helper do?

  • The ini_get() call reads one PHP configuration option.
  • A false result becomes Not available so the dashboard can explain a missing option.
  • An empty result becomes Off or empty so the table does not display a blank cell.
  • Save index.php.
  • Check the helper by running:
C:\php\php.exe -l index.php

What does success confirm?

A successful syntax check confirms that the helper's return type and conditional branches are valid PHP. The next array can now call it for each setting.

You should see No syntax errors detected in index.php again.

  • In index.php, find the closing ]; for $vulnerabilities.
  • Add the five evidence records immediately below it:
$evidence = [
    ['name' => 'memory_limit', 'value' => readSetting('memory_limit'), 'why' => 'PHP documents -1 as temporary risk reduction for a confirmed OOM path, not a complete fix.'],
    ['name' => 'post_max_size', 'value' => readSetting('post_max_size'), 'why' => 'The advisory recommends conservative request-size limits.'],
    ['name' => 'upload_max_filesize', 'value' => readSetting('upload_max_filesize'), 'why' => 'The advisory says to keep this below post_max_size.'],
    ['name' => 'session.upload_progress.enabled', 'value' => readSetting('session.upload_progress.enabled'), 'why' => 'Disabling it and restarting workers blocks the demonstrated session path only.'],
    ['name' => 'fatal_error_backtraces', 'value' => readSetting('fatal_error_backtraces'), 'why' => 'On PHP 8.5, disabling it and restarting workers blocks the confirmed backtrace path only.'],
];

Why collect these settings?

The five records capture memory limits, request-size limits, upload behavior, session upload progress, and fatal error backtraces. Each record also explains why an assessor would preserve that value as evidence.

These values describe temporary risk-reduction signals. The dashboard keeps the update-first message visible because settings cannot replace the fixed release.

  • Save index.php.
  • Check the evidence array by running:
C:\php\php.exe -l index.php

What does this result prove?

The syntax check confirms that all five array records are complete. It also confirms that every call to readSetting() is placed inside valid PHP.

You should see No syntax errors detected in index.php.

The evidence array now holds the live values. A table makes those values readable beside the reason each one matters.

  • In index.php, find the closing </section> below the CVE card loop.
  • Add the configuration evidence section immediately after it:
<section>
<p class="label">Configuration evidence</p><h2>Temporary risk-reduction signals</h2>
<p class="notice">Only an update is a complete mitigation. These values support an assessment but do not prove an unpatched runtime is safe.</p>
<div class="table-wrap"><table>
<thead><tr><th>Setting</th><th>Value</th><th>Why record it</th></tr></thead>
<tbody><?php foreach ($evidence as $item): ?><tr><td><code><?= escapeHtml($item['name']) ?></code></td><td><?= escapeHtml($item['value']) ?></td><td><?= escapeHtml($item['why']) ?></td></tr><?php endforeach; ?></tbody>
</table></div>
</section>

How does the evidence table work?

  • The warning keeps the complete mitigation visible above the evidence.
  • The foreach loop creates one row for each setting.
  • The first cell names the setting. The second cell shows its live value.
  • The final cell explains how the value supports the assessment.
  • Save index.php.
  • Return to the local dashboard in Microsoft Edge.
  • Refresh http://localhost:8000.

You should see a warning followed by five configuration rows. The rows should run from memory_limit through fatal_error_backtraces.

The final evidence block gives readers a direct route to the PHP advisory that defines the fixed releases.

  • In index.php, find the closing </section> below the evidence table.
  • Add the primary source section immediately after it:
<section class="sources">
<p class="label">Primary source</p><h2>Official PHP security advisory</h2>
<a href="https://www.php.net/archive/2026.php#2026-10-21-5" target="_blank" rel="noreferrer">Open the 21 October 2026 advisory</a>
</section>

Why include a primary source?

The link lets a reviewer compare the dashboard's fixed releases with the official PHP advisory. This makes the verdict traceable to its source.

The new cards and evidence need styles that preserve the existing dark layout. The table also needs horizontal scrolling on narrow screens.

  • In index.php, select everything between the existing <style> tags.
  • Replace the selected styles with this complete style line:
:root{color-scheme:dark;font-family:Segoe UI,sans-serif;background:#07111f;color:#e5eefb}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at top right,#12304a,#07111f 42%);min-height:100vh}a{color:#7dd3fc}.container{width:min(1080px,calc(100% - 32px));margin:auto;padding:48px 0}.hero{max-width:820px}.eyebrow,.label{color:#38bdf8;font-size:.78rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.subtitle,.card p,.status p,td{color:#afc0d8;line-height:1.6}h1{font-size:clamp(2.2rem,6vw,4.2rem);line-height:1;margin:.3em 0}section{margin-top:32px}.status,.card,.notice,.table-wrap,.sources{border:1px solid #263a55;border-radius:16px;background:#0d1a2de8}.status{display:grid;grid-template-columns:1.3fr 1fr;gap:24px;padding:24px;border-left-width:6px}.status.patched{border-left-color:#22c55e}.status.risk{border-left-color:#f43f5e}.status.unknown{border-left-color:#f59e0b}.details{margin:0}.details div{display:flex;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid #263a55}.details dd{margin:0;font-weight:800}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.card,.sources{padding:22px}.value{font:800 1.3rem Consolas,monospace;color:#f8fafc!important}.meta{display:flex;gap:8px;flex-wrap:wrap}.meta span{border:1px solid #3b526f;border-radius:999px;padding:5px 9px;font-size:.8rem}.notice{padding:16px;color:#fde68a;border-color:#9a6b13}.table-wrap{overflow-x:auto}table{width:100%;border-collapse:collapse}th,td{padding:14px;text-align:left;vertical-align:top;border-bottom:1px solid #263a55}th{background:#101e33}code{color:#bae6fd}@media(max-width:720px){.status,.grid{grid-template-columns:1fr}}

What do these styles add?

  • The shared card rules give the CVE records and source panel the same visual structure as the runtime cards.
  • The .meta rules turn the CVE identifier and severity into compact labels.
  • The .notice rule highlights the update-first mitigation message.
  • The .table-wrap rule keeps the evidence readable on narrow screens.
  • Save index.php.
  • Return to the dashboard in Microsoft Edge.
  • Refresh http://localhost:8000.

You should see two styled CVE cards. You should also see the five-row evidence table and the official PHP advisory panel.

Evidence table not styled?

Confirm that the replacement stayed inside the existing <style> tags. Check that the table remains inside a container with class="table-wrap".

If the source panel looks unstyled, confirm that it uses class="sources".

Help me fix the dashboard styles.

Check escaping and verify the dashboard

Dynamic values can contain characters with meaning in HTML. Escaping converts those characters before the browser interprets the output.

Your existing escapeHtml() helper applies htmlspecialchars() with quote handling and UTF-8 encoding. Every dynamic value in the new cards and table uses that helper.

  • Inspect the new CVE section in index.php for calls to escapeHtml().
  • Inspect the new evidence table for calls to escapeHtml().
  • Confirm that the literal severity label remains fixed page text.

Why escape every dynamic value?

Escaping keeps runtime values and array content inside their intended text or attribute context. The same boundary protects future changes if a setting contains punctuation that the browser could otherwise interpret as markup.

✔️ Awesome, I've got everything!

Your completed index.php now combines the patch verdict with official vulnerability context and configuration evidence.

ⓧ I'd like to double check the full code

Compare your saved index.php with this complete file:

<?php

declare(strict_types=1);

function escapeHtml(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function readSetting(string $name): string
{
    $value = ini_get($name);
    if ($value === false) {
        return 'Not available';
    }
    return $value === '' ? 'Off or empty' : $value;
}

$fixedVersions = [
    '8.2' => '8.2.35',
    '8.3' => '8.3.36',
    '8.4' => '8.4.27',
    '8.5' => '8.5.12',
];

$assessedVersion = PHP_VERSION;
$releaseLine = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;

if (!isset($fixedVersions[$releaseLine])) {
    $status = 'Not covered';
    $statusClass = 'unknown';
    $requiredVersion = 'Check current guidance';
    $message = 'This release line is outside the October 2026 advisory matrix.';
} elseif (version_compare($assessedVersion, $fixedVersions[$releaseLine], '>=')) {
    $status = 'Patched';
    $statusClass = 'patched';
    $requiredVersion = $fixedVersions[$releaseLine];
    $message = 'This runtime meets the fixed-version baseline for both CVEs.';
} else {
    $status = 'At risk';
    $statusClass = 'risk';
    $requiredVersion = $fixedVersions[$releaseLine];
    $message = 'This runtime is below the fixed release. Update immediately.';
}

$vulnerabilities = [
    [
        'id' => 'CVE-2026-109618',
        'ghsa' => 'GHSA-w8hw-w9fx-j79w',
        'title' => 'RCE via OOM corruption',
        'summary' => 'Out-of-memory handling can leave request data invalid, and cleanup may later treat that invalid memory as valid.',
        'url' => 'https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w',
    ],
    [
        'id' => 'CVE-2026-109619',
        'ghsa' => 'GHSA-6wmf-4rv5-p3rp',
        'title' => 'RCE via stale global state',
        'summary' => 'Request-owned state can remain after its memory is freed and be consumed by a later request on the same worker.',
        'url' => 'https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp',
    ],
];

$evidence = [
    ['name' => 'memory_limit', 'value' => readSetting('memory_limit'), 'why' => 'PHP documents -1 as temporary risk reduction for a confirmed OOM path, not a complete fix.'],
    ['name' => 'post_max_size', 'value' => readSetting('post_max_size'), 'why' => 'The advisory recommends conservative request-size limits.'],
    ['name' => 'upload_max_filesize', 'value' => readSetting('upload_max_filesize'), 'why' => 'The advisory says to keep this below post_max_size.'],
    ['name' => 'session.upload_progress.enabled', 'value' => readSetting('session.upload_progress.enabled'), 'why' => 'Disabling it and restarting workers blocks the demonstrated session path only.'],
    ['name' => 'fatal_error_backtraces', 'value' => readSetting('fatal_error_backtraces'), 'why' => 'On PHP 8.5, disabling it and restarting workers blocks the confirmed backtrace path only.'],
];
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PHP2Shell Patch Readiness Dashboard</title>
<style>
:root{color-scheme:dark;font-family:Segoe UI,sans-serif;background:#07111f;color:#e5eefb}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at top right,#12304a,#07111f 42%);min-height:100vh}a{color:#7dd3fc}.container{width:min(1080px,calc(100% - 32px));margin:auto;padding:48px 0}.hero{max-width:820px}.eyebrow,.label{color:#38bdf8;font-size:.78rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.subtitle,.card p,.status p,td{color:#afc0d8;line-height:1.6}h1{font-size:clamp(2.2rem,6vw,4.2rem);line-height:1;margin:.3em 0}section{margin-top:32px}.status,.card,.notice,.table-wrap,.sources{border:1px solid #263a55;border-radius:16px;background:#0d1a2de8}.status{display:grid;grid-template-columns:1.3fr 1fr;gap:24px;padding:24px;border-left-width:6px}.status.patched{border-left-color:#22c55e}.status.risk{border-left-color:#f43f5e}.status.unknown{border-left-color:#f59e0b}.details{margin:0}.details div{display:flex;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid #263a55}.details dd{margin:0;font-weight:800}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.card,.sources{padding:22px}.value{font:800 1.3rem Consolas,monospace;color:#f8fafc!important}.meta{display:flex;gap:8px;flex-wrap:wrap}.meta span{border:1px solid #3b526f;border-radius:999px;padding:5px 9px;font-size:.8rem}.notice{padding:16px;color:#fde68a;border-color:#9a6b13}.table-wrap{overflow-x:auto}table{width:100%;border-collapse:collapse}th,td{padding:14px;text-align:left;vertical-align:top;border-bottom:1px solid #263a55}th{background:#101e33}code{color:#bae6fd}@media(max-width:720px){.status,.grid{grid-template-columns:1fr}}
</style>
</head>
<body>
<main class="container">
<header class="hero">
<p class="eyebrow">Defensive PHP security portfolio project</p>
<h1>PHP2Shell Patch Readiness Dashboard</h1>
<p class="subtitle">Local evidence for CVE-2026-109618 and CVE-2026-109619. No exploit payloads and no external targets.</p>
</header>
<section class="status <?= escapeHtml($statusClass) ?>">
<div><p class="label">Patch verdict</p><h2><?= escapeHtml($status) ?></h2><p><?= escapeHtml($message) ?></p></div>
<dl class="details">
<div><dt>Assessed</dt><dd><?= escapeHtml($assessedVersion) ?></dd></div>
<div><dt>Release line</dt><dd><?= escapeHtml($releaseLine) ?></dd></div>
<div><dt>Required</dt><dd><?= escapeHtml($requiredVersion) ?></dd></div>
</dl>
</section>
<section>
<p class="label">Runtime inventory</p><h2>What is actually running?</h2>
<div class="grid">
<article class="card"><h3>PHP version</h3><p class="value"><?= escapeHtml(PHP_VERSION) ?></p></article>
<article class="card"><h3>Server API</h3><p class="value"><?= escapeHtml(PHP_SAPI) ?></p></article>
</div>
</section>
<section>
<p class="label">Official context</p><h2>Why the fixed releases matter</h2>
<div class="grid">
<?php foreach ($vulnerabilities as $vulnerability): ?>
<article class="card">
<div class="meta"><span><?= escapeHtml($vulnerability['id']) ?></span><span>CVSS 9.2 Critical</span></div>
<h3><?= escapeHtml($vulnerability['title']) ?></h3>
<p><?= escapeHtml($vulnerability['summary']) ?></p>
<a href="<?= escapeHtml($vulnerability['url']) ?>" target="_blank" rel="noreferrer">Read <?= escapeHtml($vulnerability['ghsa']) ?></a>
</article>
<?php endforeach; ?>
</div>
</section>
<section>
<p class="label">Configuration evidence</p><h2>Temporary risk-reduction signals</h2>
<p class="notice">Only an update is a complete mitigation. These values support an assessment but do not prove an unpatched runtime is safe.</p>
<div class="table-wrap"><table>
<thead><tr><th>Setting</th><th>Value</th><th>Why record it</th></tr></thead>
<tbody><?php foreach ($evidence as $item): ?><tr><td><code><?= escapeHtml($item['name']) ?></code></td><td><?= escapeHtml($item['value']) ?></td><td><?= escapeHtml($item['why']) ?></td></tr><?php endforeach; ?></tbody>
</table></div>
</section>
<section class="sources">
<p class="label">Primary source</p><h2>Official PHP security advisory</h2>
<a href="https://www.php.net/archive/2026.php#2026-10-21-5" target="_blank" rel="noreferrer">Open the 21 October 2026 advisory</a>
</section>
</main>
</body>
</html>

How should I use this file?

Use this full file as a comparison reference. Its functions, arrays, styles, and page sections match the completed core dashboard for this step.

Before you run the final check, do you expect PHP to accept every new array and loop?

  • Save index.php.
  • Run the final syntax check:
C:\php\php.exe -l index.php

What should the final check prove?

This check parses the complete file after every PHP addition. A successful result confirms that the helper, arrays, conditions, and loops form valid syntax.

You should see No syntax errors detected in index.php.

  • Return to Microsoft Edge.
  • Refresh http://localhost:8000.
  • Confirm that the verdict still reads Patched.
  • Confirm that the page shows two CVE cards.
  • Confirm that the page shows five configuration rows.
  • Confirm that the page shows the complete-mitigation warning.
  • Confirm that the page shows the official PHP advisory link.

Strong work. Your dashboard now supports its patch verdict with official vulnerability context and live configuration evidence.

Your local assessment is complete. Next, you will document the project and prepare it as a public portfolio artifact.

Document and Publish the Portfolio Project

Your dashboard now turns local runtime data into an evidence-backed patch verdict. Its output still exists only on your Windows device.

This step turns that local work into a reproducible portfolio artifact on GitHub. The public repository explains the result without exposing the localhost diagnostic page.

In this step, get ready to:
  • Capture the completed dashboard and document the project.
  • Commit the portfolio files and inspect the local history.
  • Publish the repository and verify its public contents.
Capture the dashboard and write the README

A screenshot shows the finished assessment without turning its runtime details into a public endpoint. You will store that evidence at docs/dashboard.png inside the repository.

  • In Visual Studio Code from earlier, click the new folder icon beside php2shell-readiness-dashboard in the Explorer sidebar.
  • Enter docs as the folder name and press Enter.

You will see the new docs folder in the repository tree.

  • Switch back to Microsoft Edge from earlier.
  • Confirm http://localhost:8000 still shows the completed dashboard.

The page should show the Patched verdict for PHP 8.5.12. You should also see the runtime inventory, both CVE cards, and the configuration evidence table.

  • Press Ctrl+- until the verdict, runtime cards, CVE cards, and evidence table fit in the browser window.
  • Press the Windows key to open Windows search.
  • Type Snipping Tool and press Enter to open Windows Snipping Tool.

You will see the capture controls for Windows Snipping Tool.

  • Drag across the Microsoft Edge window to capture the completed dashboard.

The capture preview should show the dark dashboard with its patch verdict and supporting evidence.

  • Click the save icon in the top toolbar.
  • Select the docs folder inside php2shell-readiness-dashboard.
  • Enter dashboard.png as the file name.
  • Click Save.
  • Return to Visual Studio Code from earlier.

You will see docs/dashboard.png in the Explorer sidebar. That capture now gives repository visitors a safe view of your completed assessment.

A README gives another person enough context to understand the dashboard and reproduce it locally. The file uses Markdown to combine project details with the screenshot you just saved.

  • Select README.md in the Explorer sidebar.
  • Replace the starter sentence with this project introduction:
# PHP2Shell Patch Readiness Dashboard

A defensive PHP portfolio project for assessing a local runtime against the fixed releases for CVE-2026-109618 and CVE-2026-109619.

![Dashboard screenshot](docs/dashboard.png)

## What it demonstrates

- Runtime inventory with `PHP_VERSION` and `PHP_SAPI`
- Patch assessment with `version_compare()`
- Configuration inspection with `ini_get()`
- HTML output encoding with `htmlspecialchars()`
- Evidence-backed CVE communication

What does this documentation do?

  • The opening sentence defines the project as a defensive local assessment.
  • The screenshot reference loads docs/dashboard.png when GitHub renders the README.
  • The capability list names the PHP techniques demonstrated by the source code.
  • Save README.md by pressing Ctrl+S.
  • Confirm the initial GitHub Desktop sentence has been replaced by the project title and capability list.

Still see the starter sentence?

Make sure you replaced the existing sentence in README.md before pasting the new content. Check that you saved the file inside the repository that contains index.php.

Help me fix my README introduction.

  • Add the fixed releases and local run instructions below the existing capability list:

## Fixed releases

| PHP line | Fixed release |
| --- | --- |
| 8.2 | 8.2.35 |
| 8.3 | 8.3.36 |
| 8.4 | 8.4.27 |
| 8.5 | 8.5.12 |

## Run locally on Windows

```powershell
C:\php\php.exe -S localhost:8000
```

Open `http://localhost:8000`. Stop the server with Ctrl+C.

Why include these instructions?

  • The fixed-release table makes the patch baselines easy to inspect.
  • The local run section gives another Windows user the exact command needed to start the dashboard.
  • The localhost address keeps the demonstration tied to the development server on the user's machine.
  • Save README.md by pressing Ctrl+S.
  • Confirm the file now lists four fixed releases and the local server command.

Does the table look incomplete?

Check that every table row begins and ends with a pipe character. Confirm all four release lines appear below the header row.

Help me repair the fixed-release table.

  • Add the safety boundary and primary sources below the local run instructions:

## Safety boundary

This repository contains no exploit payloads and scans no external systems. The dashboard runs on localhost because runtime and configuration details should not be exposed through a public diagnostic endpoint.

## Primary sources

- [Official PHP advisory](https://www.php.net/archive/2026.php#2026-10-21-5)
- [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)
- [NCSC-NL php2shell information](https://github.com/NCSC-NL/php2shell)

Configuration evidence supports triage, but updating is the only complete mitigation.

What does this final section prove?

  • The safety boundary states that the repository contains no exploit payloads or external scanning.
  • The source list connects the assessment to the official advisories used by the dashboard.
  • The closing sentence keeps patching at the center of the remediation message.
  • Save README.md by pressing Ctrl+S.
  • Confirm the file ends with four source links and the complete-mitigation statement.

Are the sources missing?

Check that the primary sources section sits below the safety boundary. Make sure each Markdown link keeps its URL inside parentheses.

Help me check the README source links.

✔️ Awesome, I've got everything!

Your saved README.md now documents the dashboard, fixed releases, local run process, safety boundary, and official sources.

ⓧ I'd like to double check the full code

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

A defensive PHP portfolio project for assessing a local runtime against the fixed releases for CVE-2026-109618 and CVE-2026-109619.

![Dashboard screenshot](docs/dashboard.png)

## What it demonstrates

- Runtime inventory with `PHP_VERSION` and `PHP_SAPI`
- Patch assessment with `version_compare()`
- Configuration inspection with `ini_get()`
- HTML output encoding with `htmlspecialchars()`
- Evidence-backed CVE communication

## Fixed releases

| PHP line | Fixed release |
| --- | --- |
| 8.2 | 8.2.35 |
| 8.3 | 8.3.36 |
| 8.4 | 8.4.27 |
| 8.5 | 8.5.12 |

## Run locally on Windows

```powershell
C:\php\php.exe -S localhost:8000
```

Open `http://localhost:8000`. Stop the server with Ctrl+C.

## Safety boundary

This repository contains no exploit payloads and scans no external systems. The dashboard runs on localhost because runtime and configuration details should not be exposed through a public diagnostic endpoint.

## Primary sources

- [Official PHP advisory](https://www.php.net/archive/2026.php#2026-10-21-5)
- [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)
- [NCSC-NL php2shell information](https://github.com/NCSC-NL/php2shell)

Configuration evidence supports triage, but updating is the only complete mitigation.
Commit the portfolio files

Git records a named snapshot of your repository. GitHub Desktop lets you review that snapshot before it leaves your computer.

  • Press the Windows key to open Windows search.
  • Type GitHub Desktop and press Enter to open it.
  • Use the repository selector to choose php2shell-readiness-dashboard.
  • Select Changes to view the uncommitted files.
  • Review index.php, README.md, and docs/dashboard.png in the changed-file list.
  • Inspect the changes for passwords, tokens, personal paths, or other secrets.
  • Enter Build PHP2Shell patch readiness dashboard in the commit summary field.
  • Click the commit button for the current branch.

Your files are now stored in local Git history under a descriptive commit. That snapshot is ready for inspection before publication.

Before you inspect the history, what commit title do you expect to see at the top?

  • Select History in GitHub Desktop.

You will see Build PHP2Shell patch readiness dashboard as the latest commit.

Are files missing from the commit?

Return to Changes and check whether any files remain uncommitted. If the screenshot is absent, confirm dashboard.png is inside the repository's docs folder.

Help me find an uncommitted portfolio file.

Publish and verify the repository

Publishing makes the source and documentation visible to anyone with the repository link. Your PHP development server remains available only through http://localhost:8000 while the PowerShell process is running.

What becomes public?

The repository exposes index.php, README.md, and docs/dashboard.png publicly. Your earlier secret review protects passwords, tokens, and personal paths from accidental publication.

The localhost dashboard stays on your computer. Repository visitors receive the source and screenshot without receiving a live diagnostic endpoint.

  • Click Publish repository in GitHub Desktop.
  • Confirm the repository name is php2shell-readiness-dashboard.
  • Leave Keep this code private unselected.
  • Click Publish repository to create the public repository.

GitHub Desktop will show that the repository has been published. The local history now has a public GitHub remote.

Before you open the published repository, which three project files do you expect to find there?

  • Open the Repository menu in GitHub Desktop.
  • Select View on GitHub.
  • Confirm the repository root lists index.php and README.md.
  • Open the docs folder.
  • Open dashboard.png to confirm the completed dashboard screenshot loads.
  • Return to the repository root.
  • Confirm the latest commit is Build PHP2Shell patch readiness dashboard.

You will see a public repository containing index.php, README.md, and docs/dashboard.png under the latest commit. You have turned the local assessment into a portfolio project another person can inspect and reproduce.

Does the public repository look incomplete?

Return to GitHub Desktop and inspect Changes for files that were left out of the commit. If the commit exists locally but the website is older, click Push origin.

Help me fix my published repository.

Secret mission

Add Safe Version Simulation

Your patched runtime always produces the same verdict. Add three allowlisted assessment profiles so you can safely demonstrate an at-risk result beside a patched result without installing vulnerable software.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep the dashboard available, pause its local server, or remove the project entirely. This project uses local tools plus a free GitHub account. There are no ongoing project costs.

Resources you used:

  • The localhost PHP server running in PowerShell.
  • The local php2shell-readiness-dashboard repository with index.php, README.md, docs/dashboard.png, and its .git history.
  • The patched PHP 8.5.12 installation in C:\php.
  • The public php2shell-readiness-dashboard repository on GitHub.

Keep everything running

No action is needed. Choose this if you want to keep demonstrating the dashboard or referencing its published code.

  • Keep the PowerShell window running whenever you want the dashboard available at http://localhost:8000.
  • Keep the local php2shell-readiness-dashboard repository to preserve your source files.
  • Keep C:\php to run the dashboard again.
  • Keep the public repository available as your portfolio artifact.
  • Keep Visual Studio Code, GitHub Desktop, and the Microsoft Visual C++ v14 Redistributable installed for future projects.

Pause - I'll come back to this later

Stop the localhost server to free the PowerShell window. Your project files remain ready for your next session.

  • Switch back to the PowerShell window that is serving the dashboard.
  • Press Ctrl+C to stop the localhost server.

The localhost page stops responding. Your local files stay unchanged. Your public repository also stays published.

  • Return to the existing php2shell-readiness-dashboard folder in PowerShell when you are ready to continue.
  • Restart the localhost dashboard by running this command:
C:\php\php.exe -S localhost:8000

What does this command do?

PHP starts its development server on localhost. It serves the files from the current php2shell-readiness-dashboard folder.

  • Open Microsoft Edge to verify the restarted dashboard at http://localhost:8000.

You'll see the patch verdict plus the safe simulation dropdown.

Dashboard not restarting?

Check that PowerShell is currently inside the existing php2shell-readiness-dashboard folder. Also confirm that C:\php\php.exe still exists.

Ask for help with the restart command.

Delete - I don't want to use this again

Remove the project resources to start fresh. Deletion is permanent. Your published commits and local files disappear from their current locations.

  • Switch back to the PowerShell window that is serving the dashboard.
  • Press Ctrl+C to stop the localhost server.

The localhost endpoint is now offline.

  • In GitHub Desktop, open the Repository menu.
  • Choose View on GitHub to open the published repository.
  • Open the repository settings page on GitHub.
  • Scroll to the repository deletion section.
  • Use the repository deletion control to delete php2shell-readiness-dashboard.
  • Complete the confirmation prompt displayed by GitHub.

The php2shell-readiness-dashboard repository should no longer appear among your public repositories.

  • In File Explorer, return to the existing php2shell-readiness-dashboard folder.
  • Delete the php2shell-readiness-dashboard folder.
  • In File Explorer, open C:\.
  • Delete the php folder to remove the PHP installation from C:\php.

You should no longer see the local repository in its previous location. You should also no longer see C:\php.

Visual Studio Code, GitHub Desktop, and the Microsoft Visual C++ v14 Redistributable remain installed. They may support other projects on your Windows device.

Nice Work!

Nice Work!

You did it! Your defensive PHP dashboard now assesses patch readiness without exploit code or external targets.

What you learned:

  • Built a visual runtime inventory that reports the installed PHP version plus Server API from localhost.
  • Converted release data into an evidence-backed patch verdict. Connected the result to two critical CVEs plus five configuration signals.
  • Protected dynamic values with HTML output escaping. Published the source, documentation, and dashboard screenshot in a public GitHub repository.
  • Secret Mission: added a safe, allowlisted version simulator that demonstrates At risk plus Patched outcomes without changing the installed PHP runtime.

Ready to quiz yourself?