CVE
CVE-2026-128014, CVE-2026-128015, CVE-2026-128016, CVE-2026-128017, CVE-2026-128018, CVE-2026-128019, CVE-2026-128020, CVE-2026-128021, CVE-2026-128022, CVE-2026-128023, CVE-2026-128024, CVE-2026-128025, CVE-2026-128026, CVE-2026-128027, CVE-2026-128028, CVE-2026-128029, CVE-2026-128030, CVE-2026-128031, CVE-2026-128032, CVE-2026-128033, CVE-2026-128040, CVE-2026-128041, CVE-2026-128042, CVE-2026-128043, CVE-2026-128044, CVE-2026-128045, CVE-2026-128046, CVE-2026-128047, CVE-2026-128048, CVE-2026-128049, CVE-2026-128051, CVE-2026-128052, CVE-2026-128053, CVE-2026-100397, CVE-2026-107360
CWE
CWE-290, CWE-436, CWE-1104, CWE-787, CWE-416, CWE-674, CWE-248, CWE-184, CWE-352, CWE-330, CWE-346, CWE-79, CWE-502, CWE-287, CWE-305, CWE-183
Affected Surface
- FrankenPHP (Go module `github.com/dunglas/frankenphp`, Caddy module `github.com/dunglas/frankenphp/caddy`, `dunglas/frankenphp` Docker images and the standalone static binaries) before 1.13.0 (`CVE-2026-128023` dot-form header aliasing, `CVE-2026-128024`, `CVE-2026-128025`, `CVE-2026-128026`), before 1.13.2 (`CVE-2026-128014` bundled Brotli out-of-bounds write in static binaries 1.8.0+, `CVE-2026-128020`, `CVE-2026-128021`, `CVE-2026-128022`) and before 1.13.4 (`CVE-2026-128015` to `CVE-2026-128019` worker-mode memory safety and data leaks)
- `dunglas/frankenphp` Docker images before 1.1.11, which were not rebuilt when Alpine, PHP or Go shipped fixes under existing base tags (`CVE-2026-128027`)
- OpenMage Magento LTS (Packagist `openmage/magento-lts`) before 20.18.2 and 21.0.0-beta1 to before 21.0.0-beta3 (most of the 19 CVEs), and before 20.19.0 for `CVE-2026-128042`, `CVE-2026-128043` and `CVE-2026-128052`
- PyPI `rucio` from 1.14.0.post1 before 35.9.1, 36.0.0rc1 before 38.6.1, 39.0.0rc1 before 40.4.2, and 41.0.0rc1 before 41.1.1, including the `docker.io/rucio/rucio-server` image (`CVE-2026-100397`)
- RubyGems `safemode` through 2.0.3 (no fixed release; upstream PR `theforeman/safemode#75` is open) and Foreman / Red Hat Satellite 6.16, 6.17 and 6.18 that render templates through it (`CVE-2026-107360`, fixed in Red Hat `rubygem-safemode` `1.5.0-3`)
TL;DR
On 13 November 2026, VulnCheck assigned CVE IDs to two large batches of advisories that their maintainers had already published and fixed, and two more application-layer advisories went public:
- FrankenPHP (14 CVEs). The PHP app server built on Caddy published these advisories between 4 October and 5 November and fixed them in 1.13.0 through 1.13.4. The CVEs that matter most: an unauthenticated client can spoof any trusted dash-form header (
X-Forwarded-For,X-Authenticated-User) by sending it with dots instead of dashes (CVE-2026-128023, CVSS 4.0 9.1). Official Docker images before 1.1.11 were not rebuilt when Alpine, PHP or Go fixed bugs under an existing tag, so they kept shipping libraries with known CVEs (CVE-2026-128027, CVSS 3.1 9.8). Static binaries from 1.8.0 bundle the Brotli 1.2.0 decoder that has the out-of-bounds write tracked asCVE-2026-111497(CVE-2026-128014). Several worker-mode use-after-free and state-leak bugs round out the list. - OpenMage Magento LTS (19 CVEs so far). OpenMage shipped 20.18.2, 20.19.0 and 21.0.0-beta3 on 26 October with 34 advisories; VulnCheck has assigned CVEs to 19 of them so far. The headline CVEs are a GET-only CSRF on the OAuth confirm action that hands an attacker a persistent REST token for a logged-in admin (
CVE-2026-128049, CVSS 4.0 9.4), a guest.pharupload through product custom options (CVE-2026-128046, 9.2), customer confirmation keys built frommd5(uniqid())(CVE-2026-128047, 9.2) and a pending-review RSS feed served astext/htmlthat runs a guest’s script in the admin’s browser (CVE-2026-128041, 8.8). - Rucio (
CVE-2026-100397, CVSS 9.8). The scientific data management system accepted SSH challenge tokens, which any unauthenticated caller can request for any account, as bearer credentials. Fixed on 30 July in 35.9.1, 38.6.1, 40.4.2 and 41.1.1; the GitHub advisory and CVE went public on 13 November. safemodegem (CVE-2026-107360, CVSS 9.6, no fixed gem). Compound assignments such as@host.destroy ||= nilskip the Jail allow-list in Foreman and Satellite templates, so a user who can edit templates can delete hosts. Red Hat shipped patched Satellite packages; the upstream gem fix (PR #75) is still open and a public proof of concept is on GitHub.
What to do now: Upgrade FrankenPHP to 1.13.5 (the current release, which also carries 17 more fixes from 5 November, including php_server running non-PHP files as PHP) and pull a fresh dunglas/frankenphp image. Upgrade OpenMage to 20.19.0 or 21.0.0-beta3. Upgrade Rucio to a fixed LTS release and check access logs for /auth/ssh_challenge_token. On Foreman and Satellite, apply RHSA-2026:99949 / 99950 / 99952 and limit template editing to trusted admins.
Affected packages
| Ecosystem | Package | CVE | Severity | Affected | Fixed |
|---|---|---|---|---|---|
| Go / Docker | github.com/dunglas/frankenphp, dunglas/frankenphp image | CVE-2026-128023 (header aliasing) | CVSS 4.0 9.1 | < 1.13.0 | 1.13.0 |
| Docker | dunglas/frankenphp image | CVE-2026-128027 (stale base images) | CVSS 3.1 9.8 | < 1.1.11 | 1.1.11 |
| Static binary | FrankenPHP standalone binaries | CVE-2026-128014 (bundled Brotli OOB write) | CVSS 4.0 9.2 | 1.8.0 to < 1.13.2 | 1.13.2 |
| Go | github.com/dunglas/frankenphp | CVE-2026-128015, CVE-2026-128016 (UAF), CVE-2026-128017 (double free), CVE-2026-128018, CVE-2026-128019 | CVSS 4.0 6.3 to 8.3 | < 1.13.4 | 1.13.4 |
| Go | github.com/dunglas/frankenphp | CVE-2026-128020, CVE-2026-128021, CVE-2026-128022 | CVSS 4.0 6.3 to 8.2 | < 1.13.2 | 1.13.2 |
| Go | github.com/dunglas/frankenphp | CVE-2026-128024, CVE-2026-128025 (path split), CVE-2026-128026 (putenv sandbox) | CVSS 4.0 6.9 to 8.2 | < 1.13.0 (128026: 1.5.0+) | 1.13.0 |
| Packagist | openmage/magento-lts | CVE-2026-128049 (OAuth confirm CSRF) | CVSS 4.0 9.4 | < 20.18.2; 21.0.0-beta1 to < 21.0.0-beta3 | 20.18.2, 21.0.0-beta3 |
| Packagist | openmage/magento-lts | CVE-2026-128046 (guest .phar upload) | CVSS 4.0 9.2 | 20.17.0 to < 20.18.2; 21.0.0-beta1 to < 21.0.0-beta3 | 20.18.2, 21.0.0-beta3 |
| Packagist | openmage/magento-lts | CVE-2026-128047 (predictable tokens) | CVSS 4.0 9.2 | < 20.18.2; 21.0.0 betas < 21.0.0-beta3 | 20.18.2, 21.0.0-beta3 |
| Packagist | openmage/magento-lts | CVE-2026-128041, CVE-2026-128044, CVE-2026-128040 | CVSS 4.0 8.8, 8.7, 8.4 | < 20.18.2; 21.0.0-beta1 to < 21.0.0-beta3 | 20.18.2, 21.0.0-beta3 |
| Packagist | openmage/magento-lts | 13 more (CVE-2026-128028 to 128033, 128042, 128043, 128045, 128048, 128051 to 128053) | CVSS 4.0 3.1 to 7.4 | < 20.18.2 or < 20.19.0 | 20.18.2 / 20.19.0, 21.0.0-beta3 |
| PyPI | rucio | CVE-2026-100397 | CVSS 3.1 9.8 | 1.14.0.post1 to < 35.9.1; 36.0.0rc1 to < 38.6.1; 39.0.0rc1 to < 40.4.2; 41.0.0rc1 to < 41.1.1 | 35.9.1, 38.6.1, 40.4.2, 41.1.1 |
| RubyGems | safemode | CVE-2026-107360 | CVSS 3.1 9.6 | <= 2.0.3 | None upstream; Red Hat rubygem-safemode-1.5.0-3 |
Technical analysis
1. FrankenPHP: three spellings, one $_SERVER key (CVE-2026-128023)
FrankenPHP turns request headers into $_SERVER['HTTP_*'] keys in cgi.go. Its helper upper-cases the name and only replaces - with _. The advisory quotes the vulnerable code:
// internal/phpheaders/phpheaders.go (abridged): only '-' is rewritten
var headerNameReplacer = strings.NewReplacer("-", "_")
loader = otter.LoaderFunc[string, string](func(_ context.Context, key string) (string, error) {
return "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(key)) + "\x00", nil
})
The result is then passed to PHP’s php_register_variable_ex(), which has its own rewrite rule:
/* ensure that we don't have spaces or dots in the variable name (not binary safe) */
for (p = var; *p; p++) {
if (*p == ' ' || *p == '.') {
*p = '_';
So X-Forwarded-For, X_forwarded_for and X.Forwarded.For all end up as HTTP_X_FORWARDED_FOR. FrankenPHP 1.12.4 bundled Caddy 2.11.4, which drops header names that contain an underscore, and that closed off the second spelling. Nothing dropped the third. Picture an app behind a ForwardAuth-style proxy: the proxy writes the real X-Authenticated-User and strips any client copy, but it won’t recognize X.Authenticated.User: admin as the same header, and PHP reads it as HTTP_X_AUTHENTICATED_USER. The advisory calls this the same bug class as Traefik’s CVE-2026-88879. The embedded Go library mode, where Caddy isn’t involved at all, is affected too.
Version 1.13.0 also tightened CGI path splitting (CVE-2026-128025). Before the fix, a .php in the middle of a filename such as /a.php.txt/b.php could split the path inside that filename and run a script the router never matched:
if match {
- return i + splitLen
+ pos := i + splitLen
+ // the split must end the path or a whole path segment: without
+ // this, ".php" embedded in a filename ("/a.php.txt/b.php")
+ // would split inside that filename and run a script the router
+ // never matched. ...
+ if pos != pathLen && path[pos] != '/' {
+ break
+ }
+
+ return pos
}
2. FrankenPHP images and static builds that lagged their own dependencies (CVE-2026-128027, CVE-2026-128014)
These two have nothing to do with FrankenPHP’s own code. Before 1.1.11, the Docker image pipeline only rebuilt when a new base image tag appeared. Alpine, PHP and Go routinely ship fixes under an existing tag, so a dunglas/frankenphp image could carry a vulnerable libcrypto3 (the advisory cites CVE-2025-15467) long after the base image was patched. The static binaries from 1.8.0 up to 1.13.2 link the PHP brotli extension against Brotli 1.2.0. Any app that passes untrusted data to brotli_uncompress_add() can crash the entire process, and possibly worse.
Both are reminders to rebuild images on a schedule, not just when the version number changes.
3. FrankenPHP worker mode: state that outlives a request
Worker mode keeps one PHP process alive across requests, which is where most of the remaining CVEs come from. putenv() called during request shutdown left a dangling pointer to a freed environment table (CVE-2026-128015). SplTempFileObject and SplFileObject on php://temp that survived across requests had their streams freed underneath them (CVE-2026-128016). A non-form POST followed by the end of a worker loop freed SG(request_info).content_type twice (CVE-2026-128017), and per-request environment variables leaked into later requests (CVE-2026-128019). A self-referential PHP array passed to frankenphp_log() overflowed the Go stack and killed the whole server (CVE-2026-128020).
4. OpenMage: OAuth confirm over GET, .phar uploads and md5(uniqid())
OAuth CSRF (CVE-2026-128049). The admin and storefront confirm and confirmSimple actions accepted GET without a form key. Anyone holding OAuth consumer credentials (often leaked in integrations) can send a logged-in admin to a crafted URL, which approves the attacker’s request token. The attacker then exchanges it at /oauth/token for a persistent REST token bound to that admin. A related flaw let oauth_callback pass a prefix check with a host-suffix or userinfo URL, which leaked the verifier to the attacker (CVE-2026-128044).
Guest .phar upload (CVE-2026-128046). Product “File” custom options store uploads under media/custom_options with their original extension. The protected_extensions blocklist did not include phar, php8 or phps, so on servers that map .phar to PHP and serve that directory, a guest gets code execution. The fix (commit 5b6f9857) expands the custom-option blocklist and denies the directory outright:
- <forbidden_extensions>exe</forbidden_extensions>
+ <forbidden_extensions>exe,svg,svgz,xhtml,xht,xml,xsl,xslt</forbidden_extensions>
...
+ location /media/custom_options/ { deny all; }
# media/custom_options/.htaccess (new)
Order deny,allow
Deny from all
Predictable account tokens (CVE-2026-128047). Email confirmation keys came from md5(uniqid()), which carries about 20 bits of real entropy, and confirmAction() had no rate limit. That is enough to brute-force a pending customer’s key and log in as them. Password reset tokens used the non-cryptographic uniqHash(). Commit 4a8d91c6 switches the comparisons to constant time, and companion commits replace the generators:
- if ($customer->getConfirmation() !== $key) {
+ if (!is_string($key) || !hash_equals((string) $customer->getConfirmation(), $key)) {
...
- if (is_null($customerToken) || strcmp($customerToken, $resetPasswordLinkToken) !== 0 || $customer->isResetPasswordLinkTokenExpired()) {
+ if (!is_string($customerToken) || !hash_equals($customerToken, $resetPasswordLinkToken) || $customer->isResetPasswordLinkTokenExpired()) {
The rest of the batch includes stored XSS in admin pages and RSS feeds (a guest’s product review title runs in the admin’s browser when they open the pending-reviews feed, CVE-2026-128041), the free payment method accepted on orders with a non-zero total (CVE-2026-128032), and API sessions that survive user deactivation (CVE-2026-128030).
5. Rucio: a nonce that works as a password (CVE-2026-100397)
Rucio stores SSH challenge tokens in the same table as normal auth tokens, and validate_auth_token() in lib/rucio/core/authentication.py didn’t tell them apart. Anyone can request a challenge for any account by setting a header:
curl -i -H "X-Rucio-Account: root" https://rucio.example/auth/ssh_challenge_token
# 200 OK, X-Rucio-SSH-Challenge-Token: challenge-...
curl -H "X-Rucio-Auth-Token: $CHALLENGE_TOKEN" https://rucio.example/accounts/whoami
The token is valid for about ten seconds, which is plenty of time for a script. The fix (commit 4d4a5857) rejects the prefix before any lookup:
token = token.strip()
+ # SSH challenge tokens share this table but are nonces to be signed, not
+ # credentials, so they must never authenticate a request on their own.
+ if token.startswith('challenge-'):
+ raise CannotAuthenticate("Challenge tokens cannot be used for authentication.")
+
EGI’s advisory notes that every deployment exposes /auth/ssh_challenge_token by default. If you can’t upgrade, override perm_get_auth_token_ssh in the policy package to return False and reload the server.
6. safemode: compound assignment skips the Jail (CVE-2026-107360)
safemode rewrites template Ruby so every method call goes through .to_jail, which enforces an allow-list. Its parser in 2.0.3 lets the op_asgn node family through without that rewrite, and the source admits it:
# NOTE: op_asgn*/safe_op_asgn* do not insert .to_jail on the
# receiver. Setters are called directly on the real object, bypassing Jail.
:op_asgn, :op_asgn1, :op_asgn2, :op_asgn_and, :op_asgn_or,
:safe_op_asgn, :safe_op_asgn2,
The receiver is the real object, so @host.destroy ||= nil calls Host#destroy, a method Host::Jail excludes on purpose. @host.audit ||= 1 reaches the setter, and @host[0] ||= 1 calls [] and []=. Foreman renders provisioning templates, partition tables, report templates and job templates this way, so any user with template-edit rights can delete hosts or read attributes outside the allow-list. Foreman Security confirmed host deletion. A public PoC repository (googlesky/CVE-2026-107360) runs the bypass against 2.0.3.
Exposure indicators
These are vulnerability disclosures, not malware, so there are no network IOCs. Look for:
- Requests to FrankenPHP apps with header names that contain
.(for exampleX.Forwarded.For,X.Authenticated.User) in access logs or WAF logs. dunglas/frankenphpimages whose image creation date is older than the latest base image update, and any image tag below1.13.5.- Files under
media/custom_options/with.phar,.php8,.phpsor.svgextensions on OpenMage stores, andGETrequests to/oauth/authorize/confirmor/admin/oauth_authorize/confirm. GET /auth/ssh_challenge_tokenin Rucio access logs from IPs that never complete an SSH login.- Foreman templates containing
||=,&&=or+=on template objects (@host,@subnetand so on).
Detection and remediation
# FrankenPHP: Go module, Docker image and binary versions
go list -m github.com/dunglas/frankenphp github.com/dunglas/frankenphp/caddy 2>/dev/null
docker images --format '{{.Repository}}:{{.Tag}} {{.CreatedAt}}' | rg 'dunglas/frankenphp'
frankenphp version 2>/dev/null
# Dot-form header probes in Caddy / FrankenPHP JSON logs
rg -n '"[A-Za-z0-9]+\.[A-Za-z0-9.]+":\s*\[' /var/log/caddy/*.log | rg -i 'forwarded|auth|user|api'
# OpenMage version and dangerous uploads
composer show openmage/magento-lts 2>/dev/null | rg '^versions'
find media/custom_options -type f \( -iname '*.phar' -o -iname '*.php*' -o -iname '*.phps' -o -iname '*.svg*' \) 2>/dev/null
# Rucio version and challenge-token requests
pip show rucio 2>/dev/null | rg '^Version'
rg -n 'GET /auth/ssh_challenge_token' /var/log/httpd/*access* 2>/dev/null
# safemode gem and compound assignments in Foreman templates
gem list safemode; bundle exec gem list safemode 2>/dev/null
rg -n '@[a-z_]+(\.[a-z_]+|\[[^]]*\])\s*(\|\|=|&&=|\+=|-=)' /usr/share/foreman/app/views/unattended 2>/dev/null
- FrankenPHP: upgrade to
1.13.5, rebuild images from a freshly pulleddunglas/frankenphpbase, and stop trusting identity or client-IP headers that the proxy hasn’t explicitly set. Strip any header with a.in its name at the edge if your proxy can do it. - OpenMage: upgrade to
20.19.0(or21.0.0-beta3), deny web access tomedia/custom_options/, rotate OAuth consumer keys and secrets, and revoke REST tokens issued since the store went live if you can’t audit them. - Rucio: upgrade to
35.9.1,38.6.1,40.4.2or41.1.1and restart every API server. Until then, overrideperm_get_auth_token_sshto deny. safemode/ Foreman / Satellite: apply RHSA-2026:99949 (6.16), RHSA-2026:99950 (6.17) or RHSA-2026:99952 (6.18). On upstream Foreman, restrict template editing to admins until PR #75 is merged and released.
Timeline
- 2026-02-05: FrankenPHP publishes GHSA-x9p2-77v6-6vhf (stale base images, fixed in 1.1.11).
- 2026-07-30: Rucio releases 35.9.1, 38.6.1, 40.4.2 and 41.1.1 with the SSH challenge token fix.
- 2026-08-11: Rucio publishes GHSA-x585-4vv8-2mh2; EGI SVG and OSG issue site advisories.
- 2026-10-04: FrankenPHP 1.13.0 ships with the dot-form header, path-split and
putenvfixes. - 2026-10-21 to 2026-10-26: FrankenPHP 1.13.2 and 1.13.4 fix the Brotli, worker-mode and recursion issues.
- 2026-10-26: OpenMage releases 20.18.2, 20.19.0 and 21.0.0-beta3 and publishes 34 advisories. safemode 2.0.3 is released, still vulnerable.
- 2026-11-05: FrankenPHP 1.13.5 fixes 17 more advisories, one rated High (
php_serverruns non-PHP files as PHP, GHSA-j99q-3wr8-xj43); none has a CVE yet. - 2026-11-12: Red Hat publishes CVE-2026-107360 and Satellite errata. Foreman’s PR #75 is updated.
- 2026-11-13 (
01:09 UTC): GitHub adds GHSA-x585-4vv8-2mh2 (CVE-2026-100397) to its reviewed database. - 2026-11-13 (
10:18 UTC): VulnCheck publishes 14 FrankenPHP and 19 OpenMage CVEs to NVD.
References
- FrankenPHP GHSA-qcrp-8483-f2f2 (CVE-2026-128023)
- FrankenPHP GHSA-x9p2-77v6-6vhf (CVE-2026-128027)
- VulnCheck: FrankenPHP before 1.13.0 dot-form header aliasing
- OpenMage GHSA-grrq-9gq2-vm55 (CVE-2026-128049)
- VulnCheck: OpenMage CSRF via OAuth authorization confirm
- Rucio GHSA-x585-4vv8-2mh2 (CVE-2026-100397)
- EGI SVG Advisory-EGI-SVG-2026-39
- Red Hat Bugzilla 2547645 (CVE-2026-107360)
- theforeman/safemode PR #75
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.
References
- FrankenPHP GHSA-qcrp-8483-f2f2: authentication bypass by spoofing via dot-form header name aliasing (CVE-2026-128023)
- FrankenPHP GHSA-x9p2-77v6-6vhf: delayed propagation of security fixes in upstream base images (CVE-2026-128027)
- FrankenPHP GHSA-p746-jj3f-94p8: static binaries bundle a Brotli version vulnerable to an out-of-bounds write (CVE-2026-128014)
- VulnCheck advisory: FrankenPHP before 1.13.0 authentication bypass via dot-form header name aliasing
- FrankenPHP compare v1.12.7...v1.13.0 (splitPos path-segment fix, CVE-2026-128025)
- OpenMage GHSA-grrq-9gq2-vm55: CSRF in OAuth authorization grants persistent admin or customer REST API tokens (CVE-2026-128049)
- OpenMage GHSA-4fxg-469f-r6fc and commit 5b6f9857: protected-extension blocklist omits phar/php8/phps (CVE-2026-128046)
- OpenMage GHSA-7j66-6mw7-rgfx and commit 4a8d91c6: predictable customer confirmation key and reset tokens (CVE-2026-128047)
- VulnCheck advisory: OpenMage magento-lts before 20.18.2 CSRF via OAuth authorization confirm
- Rucio GHSA-x585-4vv8-2mh2 and commit 4d4a5857: reject SSH challenge tokens as bearer auth tokens (CVE-2026-100397)
- EGI SVG Advisory-EGI-SVG-2026-39: Rucio authentication bypass via SSH challenge tokens
- OSG-SEC-2026-08-11: CRITICAL Rucio improper authentication vulnerability
- Red Hat CVE-2026-107360 and Bugzilla 2547645: rubygem-safemode Jail allow-list bypass via op-assign expressions
- theforeman/safemode PR #75: Fixes #40029, reject unsafe compound assignments (open)