16 min read
High

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.

Dunkles 19-Zoll-Rack mit FrankenPHP-Worker-Hosts: matt-schwarze Edge-Boxen, sage-grüne Patch-Kabel an den Switch-Ports, ein Modul-Cartridge mit Kraftpaper-Mono-Label und einem einzelnen oxblood-Etikett. Status-LEDs der Switch-Frontblende leuchten ruhig pulsierend, die rechte Bildhälfte fällt in tiefes Schwarz ab und lässt Negative Space für das Title-Overlay.
AI-generated · {0}

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.2 through v1.12.2 inclusive. Container images dunglas/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/search fallback was removed entirely; every byte with a value ≥ utf8.RuneSelf is 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:

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

Operating modes — not just Docker

FrankenPHP runs in several variants, all equally affected:

Three risk profiles

  1. 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.
  2. 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.
  3. 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:

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

  1. Which FrankenPHP version is running? If 1.11.2 ≤ x ≤ 1.12.2, the host is exposed.
  2. Are there user-upload or file-storage mounts inside the web root? If yes, patch immediately.
  3. 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

QuestionAnswer → 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.

Author of this post

[Translate to English:] Foto von Kai Ole Hartwig.

Kai Ole Hartwig

Founder · Moselwal Digitalagentur · OnlyOle

Programming since 2002 – self-taught, set up my own business with KO-Web in 2012, now Moselwal. Over 100 projects, with a focus on security, performance, automation and quality.