FrankenPHP 1.12.3 closes CVE-2026-45062 — when Unicode path splitting becomes an RCE path
15 May 2026. The FrankenPHP project released version 1.12.3 today, closing GHSA-3g8v-8r37-cgjm (CVE-2026-45062, CVSS 8.1 high): two logic flaws in the CGI path-splitting function let Unicode lookalikes such as ﹒php or .php be folded onto .php — anyone who can place a file in the web root triggers unauthenticated RCE in the FrankenPHP process. We rebuilt the TYPO3 and Sylius container images and raised them to 1.12.3 via our Renovate pipeline.

TL;DR — the 90-second summary
- What is CVE-2026-45062?
A Unicode-handling bug in FrankenPHP's CGI path-splitting function
splitPos(). Two independent logic flaws cause paths with Unicode lookalike characters (e.g.﹒php,.php,.php,.ⓟⓗⓟ,.𝗽𝗵𝗽) to be mapped to.php— even though the target file is not a PHP file.- How serious?
High severity, CVSS 8.1. Unauthenticated remote code execution if an attacker can drop a file with a bypass-named extension into the FrankenPHP web server directory. Public exploit code in the advisory, complete proof-of-concept with Docker. CVSS vector: AV:N/AC:H/PR:N/UI:N/C:H/I:H/A:H.
- Who is affected?
FrankenPHP versions
v1.11.2throughv1.12.2inclusive. Container imagesdunglas/frankenphp:<tag>in that version range. PHP applications that use FrankenPHP as their application server — typically TYPO3, Sylius, Laravel and Symfony stacks running in FrankenPHP worker mode.- Prerequisite for an attack
The attacker must be able to place a file with a Unicode lookalike extension (or a non-ASCII byte after a dot) into the FrankenPHP web server directory. Typical paths in: file upload endpoints, mounted file storage, package mirrors, CMS file mounts with write access for untrusted users.
- Patch
FrankenPHP 1.12.3 — released today. Fix: the
golang.org/x/text/searchfallback was removed entirely; every byte with a value ≥utf8.RuneSelfis now treated as a non-match.- What we did at Moselwal
All TYPO3 and Sylius container images rebuilt with FrankenPHP 1.12.3; the rollout to existing customer hosts has been running since early afternoon. The restart window is shorter than for a kernel patch — only the FrankenPHP worker restarts, no reboot.
What is the problem?
FrankenPHP is a modern PHP application server built on Caddy + Go that runs PHP workers directly inside the server process. For classic PHP FastCGI routing (where the path up to .php points to a script and everything after it is passed to the script as PATH_INFO), there is a helper function splitPos() in cgi.go that determines the split point.
The faulty function
The function searches the request path for the configured split suffix (typically .php) and returns the position of the split. For ASCII-only paths this runs as a simple byte-by-byte loop. As soon as a byte has a value ≥ utf8.RuneSelf (i.e. the start of a multi-byte UTF-8 character), the function falls through to a path using golang.org/x/text/search with the search.IgnoreCase flag. This fallback contains the two bugs.
Flaw 1 — stale match variable
When the inner loop hits a non-ASCII byte and the fallback search finds nothing, the code breaks out of the inner loop — but the match variable is never reset to false. The outer code then checks if match { return i + splitLen } and returns a position as if .php had matched. The file at that actual offset is something else — e.g. name.¡.txt gets routed as PHP.
Flaw 2 — Unicode IgnoreCase folding
search.New(language.Und, search.IgnoreCase) performs Unicode equivalence matching with compatibility decomposition and case folding. That goes far beyond what the surrounding code was built for. Many code points fold to ASCII ., p, h:
﹒(small full stop) →..(fullwidth full stop) →.p(fullwidth p) →pⓟⓗⓟ(circled php) →php𝗽𝗵𝗽(mathematical sans-serif bold php) →php𝓅𝒽𝓅(mathematical script php) →php
Result: a file named shell﹒php is recognised as shell.php and handed off to PHP for execution. The file contents can be arbitrary — PHP interprets them as code.
What makes the PoC sharp
The advisory ships a complete standalone reproducer plus a full proof-of-concept with Docker, three Caddy routes and three files with bypass names. A single curl request with a URL-encoded Unicode path is enough to execute poc-match-unset.¡. or poc-search-norm.𝗽𝗵𝗽 as PHP. No authentication, no interactive step.
Who is affected?
Practically every FrankenPHP deployment within the affected version range that meets one of the three prerequisites — regardless of the operating mode.
Version range
- Affected: FrankenPHP
v1.11.2throughv1.12.2inclusive. That covers practically every release between summer 2025 and yesterday. - Not affected: versions ≤
v1.11.1(the bug was introduced with the path-splitting refactor in 1.11.2) andv1.12.3(today's patch).
Operating modes — not just Docker
FrankenPHP runs in several variants, all equally affected:
- Container images
dunglas/frankenphp:<tag>in the named version range, plus:latesttags pulled during that window — that is the most common distribution path. - Standalone binary install (static FrankenPHP binary from the GitHub releases, typically at
/usr/local/bin/frankenphp), often as a systemd service or run directly from a shell. - Go module embedding — FrankenPHP embedded as a library in a custom Go binary (
github.com/dunglas/frankenphpas a dependency). Here the version depends on your owngo.modspecification. - More complex platform packages — NixOS modules, third-party Debian/Ubuntu packages, custom RPM builds. Here the package source must document the FrankenPHP version.
Three risk profiles
- Profile A — file-upload endpoints with write access to the web server directory: CMS with file-upload modules (TYPO3
sys_file, Sylius media), avatar uploads, attachment managers. Directly exposed; patching is an emergency. - Profile B — external file storage with public delivery from inside the web server directory: S3 / MinIO buckets mounted under
/uploads/or similar. Indirectly exposed; patch within 24 hours. - Profile C — read-only web server directories with no user-controlled file drops: static TYPO3 frontends, Sylius storefronts without file uploads. Low exposure; patch in the next scheduled maintenance window.
Why this bug is personally relevant
FrankenPHP is the default choice for modern TYPO3, Sylius, Laravel and Symfony deployments in worker mode — in containers as well as bare-metal setups. Anyone who pulled a FrankenPHP binary from the GitHub releases or used a dunglas/frankenphp:* container image in the past year is highly likely to be on an affected version. A single frankenphp version call on the bare-metal install will clarify that in a second.
Impact
Full code execution inside the FrankenPHP process
As soon as the attacker has placed a file with a bypass name in the web root, a single HTTP request yields code execution inside the FrankenPHP process. That is not just read access — PHP interprets the file contents with all privileges the FrankenPHP process holds. In most container setups FrankenPHP runs as www-data or nobody, but with access to:
- the TYPO3/Sylius database credentials in the
.envfile - session tokens and CSRF keys from the cache
- Composer mirror tokens, GitHub tokens, third-party API keys (mailer, payment, etc.)
- write access to the entire
typo3temp/orvar/cache/area
Container escape is not trivial, but not ruled out either
The bug itself only yields code execution in the PHP context. A container escape would need an additional kernel or container-runtime bug. But the window between first attack and cleanup is usually wide enough for the attacker to establish persistence, exfiltrate data and move laterally — all inside the PHP context.
Logging signals are weak
The bypass requests look to a classic Apache/Caddy access log like perfectly normal requests on existing files — except the path contains a few non-ASCII bytes. Anyone without a specific detection rule for Unicode lookalike paths (see below) will see nothing unusual in the logs.
Class comparison
Structurally the bug is a direct variant of CVE-2026-24895, which the FrankenPHP project already closed in March 2026 — same file, same CGI path-splitting mechanism, different bypass vector (back then direct path confusion, now Unicode IgnoreCase folding). That suggests the layer remains under external audit and further findings are possible.
Mitigation and immediate measures
Quick start — container image update
The clean route is FrankenPHP 1.12.3. For typical Docker/Compose setups:
# 1. Pull the new image (FrankenPHP 1.12.3 as base)
docker pull dunglas/frankenphp:1.12.3-php8.4-bookworm
# 2. Rebuild and restart the container service
docker compose build --pull app
docker compose up -d app
# 3. Verify
docker exec <container> frankenphp version
# expected: FrankenPHP v1.12.3
For custom container builds with FrankenPHP as a Composer/Go dependency:
go get github.com/dunglas/frankenphp@v1.12.3
go mod tidy
# rebuild and roll out the image
If you cannot update the image right away
Temporary Caddy-side mitigation via a path rewrite that rejects any request with non-ASCII bytes in the path. In the Caddyfile:
:80 {
@nonascii path_regexp [^\x00-\x7F]
respond @nonascii 400
root * /app/public
php_server
}
This also blocks legitimate paths with umlauts or accented characters, so it is only an emergency brake — not a permanent solution. Once the 1.12.3 patch is applied this mitigation can be removed.
Before the patch: check upload directories
Even after the image update, check whether your upload directory already contains files with bypass names. A poisoned upload directory becomes harmless after the patch (the bypass files are no longer routed as PHP) — but it is a sign that an attack has already taken place:
# Search for files with non-ASCII bytes in the name in the upload directory
find /var/www/uploads -name '*[\x80-\xff]*php*' -type f
find /var/www/uploads -name '*[\x80-\xff]*' -type f | head -50
Any hits here indicate an attempted or successful exploit — document forensically, isolate the file, correlate logs around the upload timestamp.
Detection and verification
Three lead questions for fast triage
- Which FrankenPHP version is running? If
1.11.2 ≤ x ≤ 1.12.2, the host is exposed. - Are there user-upload or file-storage mounts inside the web root? If yes, patch immediately.
- Are there files with non-ASCII bytes already sitting in the upload directory? If yes, investigate forensically.
Version check inside a running container
docker exec <container> frankenphp version
# If output is 1.11.2 .. 1.12.2 — patch needed
Version check from the image tag
docker inspect dunglas/frankenphp:<tag> --format '{{.Config.Labels}}' | grep org.opencontainers.image.version
Search logs for bypass patterns
In the Caddy / access log, search for requests with non-ASCII bytes in the path or URL-encoded sequences pointing at typical bypass code points:
# Non-ASCII bytes in path (common bypass signal)
grep -P 'GET [^ ]*%[CDEF][0-9A-F]%[89AB][0-9A-F]' /var/log/caddy/access.log
# Specific bypass code points in URL-encoded form
# %ef%bc%8e = U+FF0E fullwidth full stop
# %ef%b9%92 = U+FE52 small full stop
# %ef%bd%90 = U+FF50 fullwidth p
grep -E '%ef%bc%8e|%ef%b9%92|%ef%bd%90' /var/log/caddy/access.log
# Mathematical bold php: %f0%9d%97%bd = U+1D5FD (𝗽)
grep '%f0%9d%97%bd' /var/log/caddy/access.log
Falco / eBPF detection
A Falco rule can catch the triggering sequence — a FrankenPHP process opening a file with non-ASCII bytes in the path for PHP execution is a clear signal:
- rule: frankenphp_nonascii_php_exec
desc: FrankenPHP opens file with non-ASCII bytes for PHP execution
condition: >
spawned_process and
proc.name = "frankenphp" and
fd.name contains "\u00" and
fd.name endswith ".php"
output: "FrankenPHP CVE-2026-45062 trigger pattern (fd=%fd.name proc=%proc.pid)"
priority: CRITICAL
Verifying the patch
# Inside the container, with the PoC from the advisory:
curl -i --path-as-is "http://127.0.0.1:8080/poc-search-norm.%F0%9D%97%BD%F0%9D%97%B5%F0%9D%97%BD.anything-after-payload.php/trigger"
# Pre-patch: 200 OK with PHP marker output (bug active)
# Post-patch: 404 Not Found (bug closed)Operator recommendation
Operational decision block
| Question | Answer → action |
|---|---|
Is FrankenPHP v1.11.2 through v1.12.2 running? | Yes → update to 1.12.3, restart the service within 72 hours, within 24 hours for hosts with user-upload endpoints. No → no action required. |
| Do I have file-upload or file-storage mounts inside the web server directory? | Yes → patch within 24 hours, beforehand enable the Caddy mitigation or disable uploads. No → patch within 72 hours. |
| Are there already files with non-ASCII bytes in the upload directory? | Yes → document forensically, isolate the file, correlate logs around the upload timestamp. No → standard patch path. |
| Do I have automated image or binary update pipelines? | Yes → trigger the pipeline, rollout runs. No → manual update per host (container pull or binary reinstall). |
Recommendation by setup type
Single-tenant TYPO3 hosting without file upload
Low risk profile — but CVSS 8.1 still means even with a low profile: patch within 72 hours. The threshold for "in the next regular maintenance slot" sits at medium CVEs (CVSS < 7) and on isolated internal systems. A high-severity CVE does not justify that delay — especially when moving to 1.12.3 is a 1:1 replacement without configuration changes.
TYPO3 with active file upload
High risk. Update within 24 hours. Until then: temporarily disable upload endpoints or block non-ASCII paths via Caddy.
Sylius storefront with avatar / asset upload
High risk because avatar upload typically runs without strict file-extension checks. Update within 24 hours, audit the avatar upload endpoint to see whether it lets non-ASCII filenames through.
Multi-tenant shared hosting with FrankenPHP
Maximum risk. Immediate update to 1.12.3 plus the Caddy mitigation as layered defence. Any tenant could compromise the FrankenPHP worker for all the others.
FrankenPHP behind a reverse proxy with URL normalisation
Medium risk. If the upstream proxy (NGINX, Cloudflare, AWS ALB) already rejects or normalises non-ASCII paths, the risk drops — but still patch within 72 hours. The proxy layer is an additional defence line, not a replacement.
Bare-metal FrankenPHP as a systemd service
Here a binary swap plus systemctl restart frankenphp is enough; no container rebuild required. The risk profile depends on the web server directory — the recommendation follows the profiles above.
What we did at Moselwal
Renovate detects FrankenPHP releases automatically
Our TYPO3 and Sylius containers use Renovate as a version watcher. After the disclosure, Renovate picked up the FrankenPHP 1.12.3 release automatically, opened a merge request against our image definition and triggered the CI pipeline for the image rebuild. The rollout to existing customer containers is running through the day — without a manual build trigger.
Audit of upload directories
In parallel with the rollout, an audit pass via find /var/www/uploads -name '*[\x80-\xff]*' on every existing customer host. No anomalies — no file with a Unicode bypass pattern on disk. Plus a correlation of Caddy access logs for non-ASCII paths over the past 30 days, also without hits.
Detection rule deployed
The Falco rule frankenphp_nonascii_php_exec is deployed on hosts with an active Falco agent. Plus an access-log warning rule for URL-encoded bypass patterns (%ef%bc%8e, %ef%b9%92, %ef%bd%90, %f0%9d%97%bd and others) in our log aggregator.
Why Renovate makes the difference here
Renovate-driven version updates mean: no "read the disclosure today, manually rebuild tomorrow" loop. The pipeline triggers automatically as soon as the new FrankenPHP version is available; CI builds the containers; the deployment step rolls them out. For a high-severity CVE in the worker server, that is the right order of magnitude of response time — not an hours-long emergency shift, but not a week of regular maintenance window either.
Frequently asked questions about CVE-2026-45062
Do I need the patch if my website does not allow user uploads?+
The active exploit path needs an attacker-writable file in the FrankenPHP web root. If your web root contains read-only assets only and no write access exists for untrusted users, the immediate risk is low. Patch anyway — the next variant in the same layer experience-wise tends to arrive faster than the next platform audit, and 1.12.3 is a drop-in replacement with no breaking changes.
If my upstream reverse proxy blocks non-ASCII paths — am I protected?+
Suitable as defence in depth, but not a replacement for the patch. Cloudflare, an NGINX reverse proxy or AWS ALB can be configured to reject or normalise URLs with non-ASCII bytes — that lowers the risk. But: (1) the normalisation has to be explicitly configured, it is not the default; (2) if you need legitimate multilingual URLs with umlauts or accented characters, it breaks them; (3) the patch in FrankenPHP 1.12.3 closes the bug structurally, the proxy block is just a filter in front of it.
How do I check whether an exploit attempt has already taken place?+
Three parallel checks: (1) search the upload directory for files with non-ASCII bytes in the name: find /var/www/uploads -name '*[\x80-\xff]*'. (2) Grep the Caddy access log for URL-encoded bypass patterns: grep -E '%ef%bc%8e|%ef%b9%92|%ef%bd%90|%f0%9d%97%bd' /var/log/caddy/access.log. (3) Process audit log for FrankenPHP processes that loaded unusual files (with auditd or Falco). Hits in (1) or (2) are not automatically a successful exploit, but a reason for deeper forensic review.
Why is this the second CGI path-splitting CVE in FrankenPHP within two months?+
Structurally because splitPos() is a hot-path code path that makes security-relevant decisions about content routing, implemented in a language layer (Unicode handling) where subtle bugs are easy to introduce. In March 2026 CVE-2026-24895 (direct path-confusion bypass) was fixed, now CVE-2026-45062 (Unicode IgnoreCase folding). Both have the same code location in cgi.go as their starting point. It is statistically likely that further variants will appear — we therefore keep the file on our audit watch list and have FrankenPHP patches wired into the container build pipeline as an auto-trigger.
What changes for existing customers specifically?+
Nothing — except a shorter worker restart window. We rebuilt the container images this afternoon, the rollout runs by risk profile (Profile A today, Profile B within 24 hours, Profile C in the next regular maintenance slot). You typically won't notice the service pauses because the FrankenPHP worker is graceful-restart-capable in our worker-mode setup. If you pull container images yourself and are facing the platform update — talk to us, we coordinate the window.
Conclusion
CVE-2026-45062 is a classic Unicode-handling bug: a helper function trusts a library API that seems stricter than it actually is — in this case search.IgnoreCase with Unicode equivalence folding instead of plain ASCII case folding. Two logic flaws in the same function, one patch in FrankenPHP 1.12.3, a clearly defined exploit path. Anyone running FrankenPHP between v1.11.2 and v1.12.2 with user uploads in the web root should patch today.
Structurally it is the second CGI path-splitting CVE in this layer within two months — an indicator that the file cgi.go is under active external audit. We treat that the way we treat the XFRM/ESP layer in the Linux kernel: not as a one-off incident, but as a layer that needs regular patching. Image-pin discipline, automatic rebuild triggers on FrankenPHP releases and Falco detection rules keep it manageable.
FrankenPHP stacks need a container image update now — we patch and validate.
You run TYPO3, Sylius, Laravel or Symfony on FrankenPHP and aren't sure whether your container image is patched? We audit your stack against the affected version range, roll out FrankenPHP 1.12.3, deploy the Falco detection rule and check your upload directories for bypass patterns. Talk to us.
![[Translate to English:] Foto von Kai Ole Hartwig.](/fileadmin/_processed_/e/9/csm_ole-neu_73323ad80d.jpeg)
![[Translate to English:] Dunkles Linux-Server-Rack mit drei sage-grünen Patch-Kabeln zwischen Switch-Ports; das mittlere Kabel hängt halb herausgerissen und lose vor matt-schwarzen 1U-Edge-Boxen, daneben ein deep-oxblood Label-Tag — visuelle Metapher für die dritte XFRM-LPE in drei Wochen.](/fileadmin/_processed_/9/0/csm_5b253e50be33b7376cf6c7aae4858abc60e3f4d0e7da39aec18a568f00d54050_36f920642c.jpg)


![[Translate to English:] Zwei kleine Messingplaketten mit Gleichheitszeichen nebeneinander auf Beton; eine feine Risslinie zieht zwischen ihnen durch, ein roter Faden tritt aus dem Riss; daneben ein geschlossener Messingschlüssel und eine Lederhülle im kühlen Nordlicht.](/fileadmin/_processed_/2/0/csm_c7285d7e60c2c443309d4010410950e0930b24e49d581ef2d25ed5aca58e87ae_e5872e2200.jpg)
![[Translate to English:] Hölzerner Setzkasten mit präzisem Raster aus Edelstahl-Würfeln auf glattem Beton; in drei Fächern stehen leicht abweichende Messing-Würfel gleicher Größe als stille Substitution. Daneben eine Kraftpapier-Etikette mit oxblutfarbenem Faden und eine messingfarbene Juwelierlupe im kühlen Nordlicht.](/fileadmin/_processed_/5/b/csm_0d49848511671c27dc01822451c27320dd11fa770ba1b43b9369bbf3178f8480_3232cf94e2.jpg)
![[Translate to English:] Büro-Bild mit drei Monitoren, roten Warn-Akzenten und Mosel-Fensterblick im Abendlicht](/fileadmin/_processed_/9/8/csm_1f9eb86ca04c63cb88f2e4f310316127e203cd729d7750ffad4cfad4bb076389_6b17a64f21.jpg)