When a critical vulnerability is disclosed, security information starts appearing across multiple websites. The software maintainers publish technical advisories, vendors document affected products, and hosting providers explain which updates are available. Collecting those statements and comparing them can quickly become a research project of its own.
PHP2Shell is a recent example. Two critical PHP vulnerabilities, CVE-2026-109618 and CVE-2026-109619, prompted advisories from PHP, infrastructure providers, and security vendors. Although many reference the same vulnerabilities, their guidance differs depending on the products they maintain.
In this tutorial, we'll use Python and Spidra to discover PHP2Shell advisories, retrieve their contents, extract patch information into structured JSON, and prepare a CSV file for comparison. We'll also preserve the original sources so extracted information can be checked before being used in a security report.
What is PHP2Shell?
PHP2Shell refers to two critical vulnerabilities in PHP that were publicly addressed on October 21, 2026:
- CVE-2026-109618: Remote code execution through memory corruption associated with out-of-memory handling.
- CVE-2026-109619: Remote code execution involving stale request-owned global state that can be accessed after its underlying memory has been freed.
Both vulnerabilities received CVSS v4.0 scores of 9.2. The PHP security team warned that affected installations serving web requests could be exposed to unauthenticated remote code execution.
The project released fixes in the following versions:
| PHP branch | Fixed version |
|---|---|
| PHP 8.2 | 8.2.35 |
| PHP 8.3 | 8.3.36 |
| PHP 8.4 | 8.4.27 |
| PHP 8.5 | 8.5.12 |
The official PHP security announcement recommends upgrading to a release containing the patches.
However, identifying the fixed upstream versions doesn't tell us whether every product using PHP has received an update.
Some vendors rebuild PHP for their own distributions. Others backport security fixes to older versions without changing the upstream version number. Products embedding PHP may also require separate updates.
For example, Plesk's advisory identifies patched builds of PHP 7.4.33, 8.0.30, and 8.1.34 using their build dates. An affected build and a patched build can share the same version number.
This is why we'll collect each publisher's statement rather than trying to classify every product using a simple version comparison.
Find PHP2Shell advisories with Spidra Search
Let's begin by finding the sources.
We could manually search the web and copy relevant links into a Python list. But when researching several vulnerabilities or collecting updates from multiple publishers, it's more useful to have a searchable source-discovery step.
Spidra's Search API returns structured search results, including titles, URLs, descriptions, and positions.
Install the Python SDK:
pip install spidraGet your API key from the Spidra dashboard and make it available through an environment variable:
export SPIDRA_API_KEY=spd_YOUR_API_KEYCreate search_advisories.py:
# search_advisories.py
import os
from spidra import Spidra
client = Spidra(api_key=os.environ["SPIDRA_API_KEY"])
results = client.search(
"PHP2Shell CVE-2026-109618 CVE-2026-109619 security advisory",
sources=["web"],
limit=10,
)
for page in results.data.web or []:
print(page.position, page.title, page.url)The search returned 10 results, including these first three:
1 Advisory: PHP2Shell Vulnerabilities (CVE-2026-109618, CVE-2026-109619)
https://www.sophos.com/en-us/security-advisories/sophos-sa-20261021-php2shell
2 php2shell vulnerabilities (CVE-2026-109618, CVE-2026-109619)
https://github.com/NCSC-NL/php2shell
3 PHP2Shell: PHP RCE (CVE-2026-109618, CVE-2026-109619) | Sansec
https://sansec.io/research/php2shellOther results included advisories and research from SecurityScorecard, Datadog, ISPConfig, Censys, Vercel, and Tenable.
Notice that the search results aren't all the same kind of document. Some are vendor advisories, while others are technical research or general security guidance.
That's useful for discovering sources, but we shouldn't automatically treat every returned result as an authoritative statement about a particular product.
For this project, we'll select publisher-maintained advisories and preserve their URLs throughout the extraction process.
Retrieve the advisory content before extracting fields
Before asking an extraction model to interpret security information, we need to know that the source content was retrieved correctly.
We'll use the Plesk PHP2Shell advisory as an example because it contains multiple tables covering PHP branches, affected versions, and patched releases.
Spidra Scrape can retrieve a webpage as Markdown without requiring an extraction prompt or JSON schema.
Open the Scrape playground, enter the Plesk URL, and select Markdown output.
For this page, the default retrieval didn't provide the desired result, so Stealth Mode was enabled. Spidra then returned the article, including its version tables.
Here's an excerpt of the Markdown:
## Affected product version
* PHP on Plesk Obsidian for Linux:
| Product | Affected versions | Patched versions |
| ------------- | --------------------------------- | ----------------------------- |
| PHP 8.5 | 8.5.11 and below | 8.5.12 |
| PHP 8.4 | 8.4.26 and below | 8.4.27 |
| PHP 8.3 | 8.3.35 and below | 8.3.36 |
| PHP 8.2 | 8.2.34 and below | 8.2.35 |
| PHP 8.1 (EOL) | 8.1.34 (built before 21 Oct 2026) | 8.1.34 (built on 21 Oct 2026) |
| PHP 8.0 (EOL) | 8.0.30 (built before 21 Oct 2026) | 8.0.30 (built on 21 Oct 2026) |
| PHP 7.4 (EOL) | 7.4.33 (built before 21 Oct 2026) | 7.4.33 (built on 21 Oct 2026) |This section illustrates the problem with collecting only version numbers.
PHP 8.1.34 built before October 21, 2026, is listed as affected, while PHP 8.1.34 built on that date contains the fix distributed by Plesk.
If our extraction removes the build date, we lose the information needed to distinguish those two builds.
The Markdown also contained separate tables for PHP on Windows and for the PHP components used internally by Plesk Obsidian.
With the source content available, we can move on to structured extraction.
Extract patch information into JSON
Security advisories don't follow a common HTML structure.
The PHP project publishes advisories through GitHub. Plesk uses a support article containing tables, while other vendors organize their announcements around affected products, mitigations, and investigation status.
Maintaining a separate parser for each website would require additional code, particularly when publishers change their layouts.
Spidra supports extracting information using a plain-language instruction and a JSON schema. We'll use the same schema for different publishers and check whether the results preserve their distinct information.
Our initial schema includes the publisher, product, CVE identifiers, fixed versions, patch status, recommended action, and a supporting quotation.
SCHEMA = {
"type": "object",
"properties": {
"publisher": {"type": ["string", "null"]},
"product": {"type": ["string", "null"]},
"cves": {"type": "array", "items": {"type": "string"}},
"fixed_versions": {"type": "array", "items": {"type": "string"}},
"status_statement": {"type": ["string", "null"]},
"recommended_action": {"type": ["string", "null"]},
"supporting_quote": {"type": ["string", "null"]},
},
}Nullable fields are deliberate. A publisher may acknowledge an issue without listing fixed versions, or may publish investigation findings without providing an immediate remediation.
We also want to prevent the extractor from treating general information about upstream PHP as proof that a vendor-specific product has been patched.
The extraction instruction addresses that distinction:
PROMPT = (
"Extract only what this publisher explicitly states about PHP2Shell "
"(CVE-2026-109618 and CVE-2026-109619). Identify the publisher, "
"product, and CVE identifiers actually discussed on the page. "
"List fixed versions for that product only, retaining qualifiers "
"such as build dates and update identifiers. "
"Do not treat upstream PHP fixes as proof that a vendor product "
"or individual server has been patched. "
"Use [] or null for information the page does not provide. "
"Include a short exact quotation supporting the status statement."
)You can use this prompt and schema in the Scrape playground or pass them through the Python SDK.
Let's examine what the extraction returned for two different sources.
Compare the extracted results from Plesk and PHP
First, the Plesk advisory.
Using the prompt and schema above, with Stealth Mode enabled, Spidra returned the following structured result:
{
"publisher": "Plesk",
"product": "PHP and Plesk Obsidian (sw-engine / plesk-engine)",
"cves": [
"CVE-2026-109618",
"CVE-2026-109619"
],
"fixed_versions": [
"PHP 8.5.12",
"PHP 8.4.27",
"PHP 8.3.36",
"PHP 8.2.35",
"PHP 8.1.34 (built on 21 Oct 2026)",
"PHP 8.0.30 (built on 21 Oct 2026)",
"PHP 7.4.33 (built on 21 Oct 2026)",
"Plesk Obsidian 18.0.80 Update 11",
"Plesk Obsidian 18.0.81 Update 4"
],
"status_statement": "A fix is available for most affected versions.",
"recommended_action": "Update Plesk to the latest available version.",
"supporting_quote": "A fix is available for most affected versions."
}There are three useful observations here.
First, the extraction retained the build-date conditions for the older PHP branches. That gives us more useful data than a collection of version numbers alone.
Second, it identified the two Plesk Obsidian releases that contain fixes for the PHP engine used by the control panel.
Third, it retained the publisher's wording about patch availability rather than claiming that every installation had already been updated.
However, the output also reveals a limitation in our schema. The fixed_versions array combines PHP runtime releases with Plesk Obsidian application updates. That's acceptable for a small research record, but a larger dataset should use separate entries for different products, components, and platforms.
Now compare the result from the PHP project's advisory for CVE-2026-109618.
This page was extracted without Stealth Mode using the same schema:
{
"publisher": "PHP",
"product": "php-src",
"cves": [
"CVE-2026-109618"
],
"fixed_versions": [
"8.2.35",
"8.3.36",
"8.4.27",
"8.5.12"
],
"status_statement": "The patch removes longjmp() for OOM, avoiding an early exit out of contexts that do not properly account for it.",
"recommended_action": "Updating to a PHP release containing the patch to this vulnerability is the only complete mitigation. For installations that cannot update, setting and enforcing memory_limit=-1 administratively is the primary configuration mitigation.",
"supporting_quote": "Updating to a PHP release containing the patch to this vulnerability is the only complete mitigation."
}The extraction correctly identified only CVE-2026-109618 because this advisory concerns that specific vulnerability.
It also returned the upstream PHP versions rather than including information about Plesk's packaged PHP builds.
This confirms the benefit of keeping the extraction source-specific. The same fields can hold information from different publishers without assuming those publishers are reporting the same product or deployment status.
The Plesk output also demonstrates why the extracted data still needs review. Its status_statement is well supported by the source, but its combined list of versions needs additional normalization before it could support automated product comparisons.
Collect multiple advisories with Batch scrape
So far, we've extracted the two pages individually. To build a dataset, we also need to process multiple sources and keep the results separate.
Spidra's Batch scrape accepts up to 50 URLs per batch and returns a separate result for each URL.
We can use it to process the PHP project's two GitHub advisories and the Sophos advisory.
PHP_618 = "https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w"
PHP_619 = "https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp"
SOPHOS = "https://www.sophos.com/en-us/security-advisories/sophos-sa-20261021-php2shell"
batch = client.batch_scrape(
[PHP_618, PHP_619, SOPHOS],
prompt=PROMPT,
schema=SCHEMA,
)The Python SDK waits for the batch result, so we don't have to write our own polling loop.
Each item has its own URL, status, and extracted result. We can check those fields before adding an item to the dataset.
records = []
failures = []
for item in batch.items:
if item.status == "completed":
records.append({
"source_url": item.url,
"data": item.result,
})
else:
failures.append({
"source_url": item.url,
"error": item.error,
})A batch can have an overall status of completed even when individual URLs have failed. Checking each item avoids silently treating missing sources as successfully processed.
For the Plesk source, we can make a separate Scrape request with Stealth Mode enabled. That's appropriate because the earlier retrieval required it, while GitHub did not.
PLESK = (
"https://support.plesk.com/hc/en-us/articles/"
"44397729587351-Vulnerability-CVE-2026-109618-and-"
"CVE-2026-109619-PHP2Shell-Remote-code-execution-in-"
"PHP-shipped-with-Plesk"
)
plesk = client.scrape(
PLESK,
prompt=PROMPT,
schema=SCHEMA,
use_proxy=True,
)Spidra also supports applying Stealth Mode to an entire batch, but the batch settings apply to all URLs in that request. For this example, keeping Plesk separate lets us use different retrieval settings without adding unnecessary proxy usage to the other sources.
For more on handling individual failures and retries, see our guide to scraping multiple URLs in Python.
Save the advisories as CSV and JSON
Once the requests finish, we need to save the extracted information in a form that's easy to inspect or process.
We'll preserve the source URL and the time each record was collected. New records will also have a needs_review status because a successful extraction isn't the same as an independently verified security claim.
The complete example project includes a normalization function that checks whether each extracted record references at least one of the two PHP2Shell CVEs.
The core validation looks like this:
CVES = {"CVE-2026-109618", "CVE-2026-109619"}
def validate_cves(data):
if not isinstance(data, dict):
raise ValueError("Extraction is not a JSON object")
identifiers = data.get("cves") or []
if not isinstance(identifiers, list):
identifiers = []
matches = sorted(
CVES & {str(value).upper() for value in identifiers}
)
if not matches:
raise ValueError("No PHP2Shell CVE identified")
return matchesWe don't use the extracted record if it has the wrong structure or doesn't identify either vulnerability.
After validation, the complete script writes four files:
| File | Purpose |
|---|---|
candidate_sources.csv | Search results that can be reviewed for inclusion |
php2shell_advisories.csv | Publisher-specific advisory records |
php2shell_advisories.json | The same records in a machine-readable format |
failed_sources.csv | Sources that failed retrieval or structured validation |
The exported records include the source URL, retrieval timestamp, and review status.
This provides a foundation for a security research report, but the records must be checked against their original advisories before their status claims are trusted.
You can find the complete PHP2Shell Python script on GitHub Gist. Download the script, configure your Spidra API key, and run it to collect the advisories and export the results as CSV and JSON.
Why vendor-specific details matter
The extracted Plesk and PHP results demonstrate why security advisories shouldn't be reduced to a single version list.
Consider the following information from the two sources:
| Publisher | Product | Patch information |
|---|---|---|
| PHP | Upstream PHP 8.3 | Fixed in 8.3.36 |
| Plesk | PHP 8.1 on Plesk Linux | Fixed in PHP 8.1.34 built on October 21, 2026 |
| Plesk | Plesk Obsidian control panel | Fixed in 18.0.80 Update 11 or 18.0.81 Update 4 |
All three statements relate to PHP2Shell, but they describe different software and different forms of remediation.
The first identifies an upstream PHP release. The second identifies a vendor-built PHP package whose patch status depends on its build date. The third identifies a Plesk application update.
An application that compared only version numbers could misinterpret the second statement. Another application that applied an upstream PHP fix to every product might incorrectly claim that the Plesk control panel was patched.
A useful research dataset should preserve the publisher, product, version information, and supporting source as separate pieces of evidence.
It should also preserve uncertainty. A publisher saying that an investigation is ongoing isn't equivalent to confirming that a product is affected or unaffected.
What this approach cannot verify
Collecting security advisories is different from assessing the security of an individual system.
Our application can retrieve and organize public information, but it cannot inspect a server's installed packages, identify runtime settings, or establish whether the server is vulnerable.
There are a few other limitations:
Search results aren't authoritative by themselves. A relevant result may be commentary rather than an official vendor statement. Review candidate sources before using them for product-specific claims.
Extraction can misinterpret complex documents. Tables, multiple products, and similar version numbers can lead to mistakes. Compare the extracted fields with the underlying Markdown and original webpage.
Some sources may fail. Websites can block automated retrieval or change their page structure. Failed items should be recorded and investigated rather than omitted without explanation.
Advisories can change. Publishers may update their patch information after initial disclosure. Recording when content was retrieved helps readers interpret the dataset, but it doesn't guarantee that the data remains current.
For projects focused only on GitHub-hosted advisories, the GitHub Security Advisory API may provide the necessary structured information directly. The advantage of a web-data workflow is that it can also process vendor support pages and other public sources that don't expose the same data through a common API.
Conclusion
PHP2Shell shows why researching a major vulnerability involves more than finding a CVE number and reading the initial announcement.
The PHP project publishes upstream fixes, while vendors such as Plesk document changes to the versions and products they distribute. Those differences matter when you're trying to understand what has been patched.
With Python and Spidra, we can find relevant sources, retrieve their contents, extract common fields, and save the results for comparison without building a separate webpage parser for each publisher.
The same approach can be used for other vulnerability disclosures by updating the CVE identifiers, selecting new sources, and adjusting the extraction schema.
Create a free Spidra account to run the example with your own security advisory sources.
