Back to blog

CVE-2026-85978: how one path normalization mismatch turned Akana's admin console into unauthenticated remote code execution

· · 20 min read
CVE-2026-85978: how one path normalization mismatch turned Akana's admin console into unauthenticated remote code execution

On September 9, 2026, Perforce published an advisory for an unauthenticated remote code execution flaw in the Policy Manager console of the Akana API Platform. The verdict is as bad as it gets: an attacker who can reach the console over the network, with no credentials and no user interaction, can send a single crafted request that slips past the authentication filter and lands on an endpoint that evaluates attacker-supplied script code with no sandbox around it. That is arbitrary code execution as the platform service account, on the box that governs your entire API estate. The vulnerability is tracked as CVE-2026-85978 and carries a CVSS 10.0 (Critical) score. The striking part is not the payload. It is that the payload never had to defeat the authentication check. It simply walked around it, because two components in the same request pipeline disagreed about what the URL actually said.

This is a two-layered breakdown. The first part is for readers who need to decide quickly whether this touches them and what to do about it, without wading through servlet internals. The second part reconstructs the chain the way an analyst would, from the path normalization gap to the unsandboxed script sink, with faithful, clearly labeled illustrations of the mechanism. At the end, we look at how the Pragma Core platform addresses exactly the class of problem that made this possible: a bug that lives in the disagreement between two functions, not in any single line either of them contains.


Part I. Executive breakdown

What happened

The Akana API Platform is the software many organizations use to publish, secure, and govern their APIs. It sits between the outside world and a company's backend services, deciding who gets to call what, applying rate limits, transforming messages, and enforcing security policy. The Policy Manager console is its brain: the administrative application where operators define those policies and manage the platform. It is a privileged component by design. Whoever controls Policy Manager controls how every API in the estate is secured.

Like most Java web applications, the console protects its sensitive pages with an authentication filter. Think of it as a guard standing at the door, checking a list of protected paths and demanding credentials before letting a request through to the page behind it. Deeper inside the application, a separate component, the servlet dispatcher, reads the same request and decides which piece of code should actually handle it. The flaw is that the guard and the dispatcher do not read the address on the request the same way. The guard normalizes the path one way, decides the request is harmless, and waves it through. The dispatcher normalizes it a different way and routes it to a protected, privileged endpoint the guard never intended to expose. The request was allowed by one component and honored by another, and the two never agreed on where it was going.

The endpoint it reaches is worse than a data leak. It is an endpoint that takes script code and runs it, and it runs that script without a sandbox to constrain what the script can do. So the attacker does not just reach an admin page without logging in. They reach a page that executes whatever code they hand it, on the server, as the account the platform runs under. The concrete result: unauthenticated arbitrary code execution on the host that governs the organization's APIs.

Who is affected

Component Status
Akana API Platform, Policy Manager console Vulnerable. Apply the fixed build from the Perforce advisory immediately, or restrict network access to the console until you can.
Network Director (runtime gateway containers) Not the vulnerable component, but co-located deployments raise blast radius. Review network segmentation.
Community Manager (developer portal) Not the vulnerable component. Review only if it shares a host or trust zone with Policy Manager.
Deployments where Policy Manager is not network reachable by untrusted clients Lower immediate risk, but still patch. Internal reachability is enough for an attacker with a foothold.

The public CVE record did not enumerate exact version ranges at the time of writing, and directs administrators to Perforce's advisory for the specific fixed builds. Do not wait for a version table to circulate before you act. The relevant number here is not a version count, it is the precondition count: zero. No credentials, no user interaction, no chained bug. A vulnerability that reaches CVSS 10.0 does so because every dimension of impact is at maximum and every barrier to reaching it is at minimum. When the only thing standing between an attacker and code execution is network reachability, network reachability becomes your entire security boundary.

Why this matters beyond Akana

The specific product here is an API management platform, but the pattern is one of the most durable and most underappreciated bug classes in web security: authentication bypass through path normalization disagreement. Any time a request passes through two or more components that each parse and normalize the URL independently, and any time those components use the result of that parsing to make different decisions (one deciding "is this protected," the other deciding "who handles this"), you have the raw material for this bug. It has surfaced in reverse proxies fronting application servers, in security filters layered over servlet containers, in API gateways in front of microservices, and in service meshes routing between sidecars. The common denominator is not a language or a framework. It is a trust boundary drawn on a parsed representation of input, where the parser on one side is not the parser on the other.

The second ingredient, an endpoint that evaluates attacker-influenced script without a sandbox, turns what would otherwise be an information disclosure or a privilege issue into full remote code execution. Scripting policies are a genuinely useful feature in API platforms; they let operators write transformation and routing logic. But every scripting surface is a code execution surface, and the only question is who is allowed to reach it. This bug answers that question with "anyone on the network."

Recommended actions

  1. Upgrade to the fixed Akana API Platform build named in the Perforce advisory as your first and primary action. This is a design-level fix in how the request path is validated, and no configuration tweak fully substitutes for it.
  2. Immediately restrict network access to the Policy Manager console to a management network, VPN, or explicit allowlist. Akana's own hardening guidance states the console is not meant to be internet facing; enforce that at the network layer, not just by convention.
  3. Inventory every Policy Manager instance you run, including staging, disaster recovery, and forgotten proof-of-concept deployments. The instance you do not know about is the one that is exposed.
  4. Audit access logs on the console for requests containing path traversal or path parameter artifacts (..;, %2e, ..%2f, doubled slashes, trailing dots) aimed at administrative or scripting endpoints, and for unexpected requests to any script evaluation route.
  5. Constrain the scripting policy configuration to the minimum set of languages you actually use, following Akana's recommendation to limit supported scripting languages. This shrinks the sink even if a bypass is found again.
  6. Treat any confirmed exploitation as a full host compromise. Rotate the platform service account credentials, any secrets accessible from that host, and any keys the platform used to sign or broker API traffic.

Part II. Technical breakdown

Background: how a servlet application draws its authentication boundary

To understand this bug, you have to understand where a Java web application decides that a request is allowed. In a typical servlet deployment, an incoming HTTP request passes through a chain of filters before it reaches the servlet that generates the response. A security filter is the standard place to enforce authentication: it inspects the request path, checks whether that path matches a protected pattern, and either demands a valid session or lets the request continue down the chain.

The subtlety is which representation of the path the filter inspects. The servlet API exposes several: getRequestURI() returns the raw, undecoded path as it appeared on the request line; getServletPath() and getPathInfo() return values derived after the container has mapped the request to a servlet, which involves its own decoding and normalization; and the container's internal dispatcher performs yet another normalization when it decides which servlet actually handles the request. These are not guaranteed to agree. They were designed for different purposes at different layers, and the differences between them are exactly the seams a path normalization bypass lives in.

Now layer the Akana architecture on top. Policy Manager runs as a servlet application inside a Jetty-based container. Its authentication filter guards the administrative surface. Somewhere behind that filter sits the platform's scripting capability, the feature that lets operators attach script-based policies (JavaScript, and historically Jython and other engines) to evaluate and transform API traffic. That scripting capability is reachable through an internal endpoint. The design invariant, the thing the whole security model silently depends on, is that the authentication filter's idea of "which paths are protected" must cover every path the dispatcher can route to a privileged endpoint. CVE-2026-85978 is what happens when that invariant is false.

The vulnerability: a normalization gap between the filter and the dispatcher

The root cause, in the words of the advisory, is "a path normalization discrepancy between the authentication filter and the servlet dispatcher." Let us make that concrete. Path normalization is the process of resolving a URL path into its canonical form: collapsing . and .. segments, folding duplicate slashes, decoding percent-encoded characters, and handling container-specific oddities like path parameters (the ;key=value syntax that Java servlet containers accept on any path segment).

The bug is a classic mismatch of the following shape. The authentication filter examines the request path and asks a question like "does this begin with the protected admin prefix, and if not, is it a public path I can skip?" The dispatcher, later, resolves the same path to a servlet mapping. If the filter normalizes the path in a way that makes a protected route look like an unprotected one, while the dispatcher normalizes it back into the protected route, the request is judged public by the guard and private by the router.

The following is an illustrative reconstruction of the mismatch, not the vendor's source, which is not public. It shows the class of error precisely:

// Illustrative reconstruction of the mismatch (not vendor source).

// Authentication filter: decides whether the path is protected.
public void doFilter(HttpServletRequest req, ...) {
    String path = req.getServletPath();   // one normalization
    if (isPublicPath(path)) {
        chain.doFilter(req, resp);        // skip auth, pass through
        return;
    }
    requireAuthenticatedSession(req);     // otherwise enforce login
}

// Later, the container/dispatcher resolves the target servlet
// using a DIFFERENT view of the same request URI:
//   raw:        /admin/..;/scriptservice/eval
//   filter sees (getServletPath, decoded/normalized): "/"  or a
//               path that isPublicPath() happily accepts
//   dispatcher resolves (after its own normalization):
//               /scriptservice/eval   <-- the protected endpoint

The specific trick that produces the divergence varies by container and version, and several well-known variants all fit this template. The path parameter form /protected/..;/target is a recurring offender: the ; starts a path parameter that some normalizers strip and some do not, and the .. that follows may be resolved before or after the strip. Percent-encoded traversal (%2e%2e%2f), doubled slashes (//admin), trailing dots, and mixed-case encodings are the other usual suspects. In every case, the attacker's goal is identical: find one spelling of the target URL that the filter reads as harmless and the dispatcher reads as the privileged route.

The sink: an unsandboxed script evaluation endpoint

Bypassing authentication is only half the vulnerability. What makes CVE-2026-85978 a 10.0 rather than an information disclosure is where the bypass lands. The advisory states the reached endpoint "evaluates attacker-supplied script code without sandboxing." Akana's platform supports scripting policies as a first-class feature, and the documentation explicitly discusses limiting which scripting languages are enabled, evidence that a general script evaluation capability lives inside the platform.

An endpoint that takes a script body and evaluates it is, by definition, a code execution primitive for whoever can reach it. Normally the authentication filter is the thing that ensures "whoever can reach it" means "an authenticated administrator." Strip that guarantee away and the endpoint becomes an unauthenticated eval. The "without sandboxing" detail is what removes the last possible mitigation: even if the script engine could have been confined to a restricted set of operations (no file system, no process spawning, no network), it was not. The evaluated code runs with the full authority of the platform's Java process.

The following illustrates the combined request the way an attacker would compose it, again as a faithful reconstruction of the mechanism rather than a working exploit:

POST /admin/..;/<script-eval-endpoint> HTTP/1.1
Host: policymanager.internal:9900
Content-Type: application/x-groovy
Content-Length: <n>

// unsandboxed script body, runs as the PM service account
"whoami".execute().text

Two things are worth underlining. First, the request needs no Authorization header, no session cookie, no CSRF token. The ..;/ (or whichever normalization variant the target requires) is the entire authentication bypass. Second, the body is not an injection into a query or a template; it is the program the server was asked to run. There is no parser to confuse and no escaping to defeat at the sink. The sink's job is to run code, and it does.

Root cause: a boundary drawn on the wrong representation

Stepping back, the failure is not "the filter has a bug" or "the eval endpoint has a bug." Each component is doing what it was written to do. The filter matches paths. The dispatcher routes paths. The script endpoint runs scripts. The vulnerability is in the relationship between them: the authentication decision is made on one normalization of the input, and the routing decision is made on another, so the security check and the thing it was meant to protect are keyed on different values.

This is why the bug is invisible to a reviewer looking at any single function. Read the filter alone and it looks correct: it protects the admin prefix. Read the dispatcher alone and it looks correct: it maps URLs to servlets per the servlet spec. Read the script endpoint alone and it looks like an intended, documented feature. The defect only appears when you trace one request across all three and notice that the string the filter judged is not the string the dispatcher honored. Bugs of this shape are systematically missed by tools and reviewers that reason about functions in isolation, because there is no single line to point at.

Affected versions

Perforce's advisory is the authoritative source for the exact affected and fixed builds of the Akana API Platform, and administrators should map their deployment against it directly. At the time of writing, the public CVE record described the affected component (the Policy Manager console) and the impact but did not publish an enumerated version range, so we deliberately avoid quoting version numbers we cannot verify. The safe operational assumption for any unpatched Policy Manager console is that it is affected until you have confirmed otherwise against the vendor advisory and applied the fixed build.

Timeline

Date Event
Not publicly disclosed Vulnerability privately reported to Perforce
Not publicly disclosed Vendor triage, confirmation, and fix development
2026-09-09 Perforce publishes the advisory with fixed builds
2026-09-09 CVE-2026-85978 published (CVSS 10.0, Critical)

The compressed public timeline, advisory and CVE landing the same day, is the normal shape of a coordinated disclosure where the fix is ready before the record goes public. It also means defenders and attackers learn of the bug simultaneously, which is precisely why the patch window matters so much for a network-reachable, no-precondition RCE.

A note on the discovery methodology

Path normalization bypasses are found by people who refuse to accept that two components "obviously" agree about a URL. The productive technique is differential analysis: take every representation of the path the framework exposes (getRequestURI, getServletPath, getPathInfo, the dispatcher's resolved mapping), feed the same adversarial inputs through each, and diff the results. The inputs that matter are the boring ones, path parameters, dot segments, encoded separators, duplicate slashes, and the goal is to find a single input for which the "is this protected" answer and the "who handles this" answer disagree. Pairing that with an inventory of dangerous sinks (anything that evaluates, deserializes, or executes) tells you which disagreements are merely interesting and which are catastrophic. The meta-lesson for the AppSec community is that this bug class is discoverable proactively and cheaply, but only if you model requests as they flow across components rather than auditing each component against its own spec.


What we should learn from CVE-2026-85978

  1. A trust boundary is only as strong as the agreement between the parsers on either side of it. Any time two components independently parse and normalize the same input and then make different security-relevant decisions from it, treat the gap between their parsers as an attack surface until proven otherwise. Test it with the same input on both sides and diff the outcomes.

  2. Authentication filters must key on the same value the dispatcher routes on. If your security layer inspects one representation of the path and your container routes on another, you do not have an authentication boundary, you have the appearance of one. Prefer enforcing authorization at the point of dispatch, or canonicalize once and reuse that single value everywhere.

  3. Every scripting or evaluation feature is a code execution sink that must be defended as one. Useful as they are, script policies convert an access-control failure into remote code execution. Sandbox them, restrict the enabled engines to the minimum, and put explicit authorization directly on the endpoint, never solely on a filter upstream of it.

  4. Defense in depth is what keeps a bypass from becoming a breach. Network segmentation that keeps an administrative console off untrusted networks would have reduced this 10.0 to a much narrower internal risk. Controls you assume are "enough on their own" fail together; controls layered independently fail one at a time.

  5. Bugs that live in relationships are invisible to single-function review. The filter, the dispatcher, and the script endpoint are each individually defensible. The vulnerability exists only in how they compose. Any review process that cannot follow one request across component boundaries will keep missing this entire class of flaw.


How Pragma Core addresses this class of problem

CVE-2026-85978 is the archetype of a bug that classical scanners miss: there is no single bad line to flag. The filter is correct on its own terms, the dispatcher is correct on its own terms, and the script endpoint is a documented feature. The vulnerability is a property of how untrusted input flows across three components and how a security decision made on one representation of that input fails to bind the component that acts on another. Pragma Core is built for exactly this: reasoning about state and input as they cross functions, services, and trust boundaries. Here is how that maps onto this specific bug.

Autonomous AI agents for attack chain investigation

The autonomous agents do not stop at "here is an authentication filter." They ask the question that surfaces this class of bug: for every privileged endpoint, is the exact path value the dispatcher will route to guaranteed to be covered by the value the auth filter evaluated? Applied to a codebase like Policy Manager, the agent would trace from the script evaluation endpoint backward to every route that can reach it, then forward from the filter to check whether any normalization variant of a protected path escapes isPublicPath. That is the reasoning a human researcher performed here, expressed as a repeatable investigation rather than a one-off discovery.

Interactive call graphs with vulnerability overlay

Pragma Core auto-generates call graphs for a connected repository, and this bug is precisely the kind that a graph makes visible. The flaw is not in the filter node or the dispatcher node, it is in the edge between them, and in the path from an unauthenticated entry point to the eval sink. Overlaying findings onto the graph shows an operator the shortest route from "network request" to "script execution" and highlights that the authentication node on that path is keyed on a different value than the routing node. The relationship becomes something you can point at, which is exactly what single-file review cannot do.

SAST tuned for the relevant pattern, not just injection sinks

Off-the-shelf static analysis is tuned to taint flows that end in SQL or shell sinks; it is largely blind to "security decision made on normalization A, action taken on normalization B." Pragma Core's static analysis can be tuned to this pattern directly: flag any code path where an authorization check reads one path representation (getServletPath) while the eventual dispatch or handler reads another (getRequestURI, a re-parsed URI), and flag any script or expression evaluation reachable without an authorization check on the same code value. Both are high-confidence findings, and both describe CVE-2026-85978.

Continuous tracking of third-party packages and platforms

Akana is exactly the kind of platform that sits inside an enterprise stack, often owned by a different team than the one shipping the APIs on top of it, and often invisible in a typical dependency view because it is an appliance rather than a library. Pragma Core tracks third-party components and platforms across every connected repository and deployment descriptor, so when a CVE like this lands, the affected teams are notified the moment it enters the catalog, with the CVSS score and the fixed build path attached, rather than finding out from a headline.

Full SBOM per repository and deployment

The first question during response to a 10.0 is "which installations do we have and which version is each running," and most organizations cannot answer it fast enough. Pragma Core generates a complete, exportable component inventory per repository, which turns that scramble into a lookup. For an appliance like Policy Manager, the same inventory discipline applied to deployment manifests and infrastructure code shrinks the "unknown exposed instance" problem that makes bugs like this linger.

Human-guided AppSec investigations

The differential path-normalization analysis that finds bugs like this is a specialist activity, and it is exactly what Pragma Core's expert-led research module is for: an AppSec operator drives the investigation, supported by autonomous agents and the full repository context already in the workspace, focused on the surfaces the team believes are fragile. This is the same investigation the original researcher performed, made systematic and repeatable across your codebase rather than dependent on one person happening to look.


Closing thoughts

CVE-2026-85978 is not, fundamentally, a bug about Akana, or about servlets, or even about scripting policies. It is a bug about a trust boundary drawn on a representation of input that the enforcing component and the acting component did not share. That pattern is everywhere modern systems put a proxy in front of an app, a filter in front of a router, or a gateway in front of a mesh: two parsers, one input, two decisions, and a security model that quietly assumes the two decisions are keyed on the same value. When they are not, the strongest authentication check in the world protects nothing, because the request simply arrives somewhere the check never looked.

The difference between reading this as a curiosity and using it as an audit trigger comes down to AppSec maturity and code visibility: whether you can follow a single request across your own components and prove that every privileged sink is guarded on the value that actually reaches it. Organizations that want to move from "we scan and report" to "we systematically investigate what is fragile" can reach Pragma Core at pragma-core.com for a demo.


Sources

Related posts
CVE-2026-78174: how an unredacted session token in a diagnostic log turned a low-privileged WatchGuard Dimension admin into super admin
Sep 1, 2026
Qwen 3.8 27B and the year the open weights caught up: how a 17GB download started beating the frontier
Aug 19, 2026
CVE-2026-65321: how one backslash in PyAthena's quote escaper turned parameterized DELETE queries into SQL injection
Aug 3, 2026

Start securing your codebase today

Connect your repositories and let AI agents handle continuous scanning, research, and triage.

Have questions? Get in touch →