September 12, 2026
How I Tricked OpenClaw Into Attacking Its Own Network: A NAT64 SSRF Bypass
A story about how a single misread pair of bytes let a public-looking IPv6 address secretly point at 169.254.169.254.

By Anurag Mewar
7 min read
An AI assistant with keys to your whole network
OpenClaw (previously known as Clawdbot and Moltbot) runs on your devices, answers you on the channels you already use, speaks and listens on your phone and your computer, and grows with you over time. With millions of active users, and thousands of endpoints running it as an always-on gateway.
Read that again with a security hat on. "Always-on," "runs on your devices," "answers on the channels you already use". That means OpenClaw's gateway is a long-lived process sitting inside your trust boundary (on your laptop, on a home server, or on a cloud box next to your other infrastructure). And one of the interesting features that should make any security conscious individual sit up straight: "the assistant can fetch URLs on your behalf".
The web_fetch tool lets the agent go grab a web page. Image attachments in the chat completions API get downloaded server-side. Inbound channel messages can carry media URLs that the gateway resolves and pulls. Every one of those is the assistant reaching out to the network from inside your perimeter, using your machine's network position.
That is the exact shape of a Server-Side Request Forgery (SSRF) primitive. If an attacker can convince the assistant to fetch a URL of the attacker's choosing, and that URL resolves to something internal (your router's admin panel, a database bound to localhost, or the crown jewel of cloud SSRF, the metadata endpoint at 169.254.169.254) then the attacker just borrowed your server's identity to reach places they should never be able to touch.
The OpenClaw team knew this. That's why they built an SSRF guard.
The guard, and the thing it was guarding against
The whole point of the guard is simple to state: before the gateway connects to any resolved address, block anything private, internal, loopback, link-local, or otherwise special-use. Resolve the hostname, look at every IP it points to, and if any of them is 127.0.0.1, or 10.x, or 169.254.x, or a carrier-grade-NAT metadata address, refuse the request and throw.
The tricky part isn't blocking 127.0.0.1. Anyone can block a plain dotted-quad. The tricky part is that IPv6 gives an attacker a dozen ways to smuggle an IPv4 address inside an IPv6 literal. There are IPv4-mapped addresses (::ffff:127.0.0.1), 6to4, Teredo, ISATAP, and the NAT64 / IPv4-embedded transition prefixes. A serious guard has to decode the embedded IPv4 out of each of those forms and check that, not just the IPv6 wrapper.
OpenClaw's guard does exactly this. It has a function called extractEmbeddedIpv4FromIpv6, whose entire job is to pull the real IPv4 out of these transition formats so the guard can judge the true destination. It leans on a well-respected library, ipaddr.js, to classify which transition format each address belongs to.
And that is where the bug lived (not in a missing check), but in a correct-looking one that read the wrong bytes.
The moment it clicked
The guard grouped two different NAT64 formats into a single case and decoded both of them the same way by reading the last 32 bits of the address.
For the famous NAT64 prefix (the well-known 64:ff9b::/96), reading the last 32 bits is exactly right. That's where the embedded IPv4 lives. But ipaddr.js folds a second, larger prefix into the same classification: the RFC 8215 locally-assigned NAT64 prefix 64:ff9b:1::/48. And in that format, per RFC 6052, the embedded IPv4 does not live in the last 32 bits. It lives up front, split across bits 48-63 and 72-87, with a reserved byte in the middle. So the guard, for any 64:ff9b:1::/48 address, was reading the wrong end of the address entirely. If the guard reads the trailing bits to decide "safe or not safe", and those trailing bits are an ignored suffix that a real NAT64 router doesn't even route on, then an attacker gets to put two different addresses into one IPv6 literal:
- A private, internal target in the real embedded slot (bits 48-63 + 72-87): the address the NAT64 gateway will actually forward to.
- A benign public decoy (say, Google's 8.8.8.8) in the trailing bits: the only thing the guard ever looks at.
The guard sees 8.8.8.8, shrugs, and opens the gate. The router forwards to 169.254.169.254. The address, quite literally, lied.
Why this is dangerous
Given a crafted /48 literal, the guard always misreads it and always returns the decoy verdict. No timing, no race, no luck.
The end-to-end SSRF here has one precondition: the host running OpenClaw has to have an actual RFC 8215 64:ff9b:1::/48 NAT64 gateway on its network, because something has to actually forward the crafted address to the private target. On a host with that NAT64 setup, an authenticated user could steer web_fetch, image-URL ingestion, and inbound media fetches at internal and cloud-metadata endpoints (straight through a guard that was specifically built to stop exactly that).
Science behind the bypass
Now the fun part. Let's build the lying address, bit by bit, and see precisely why the guard was fooled.
How NAT64 embeds an IPv4 address (RFC 6052)
NAT64 lets IPv6-only clients talk to IPv4-only servers. To do that, the IPv4 address gets embedded inside an IPv6 address using a special prefix. The layout has a deliberate hole in it. Bits 64-71 form a reserved u-byte that must be zero. The IPv4 octets flow around that hole. For a prefix length of 48, RFC 6052 says the 32-bit IPv4 address "a.b.c.d" is placed like this:
In the green rows, the suffix bits are ignored. RFC 6052 explicitly requires translators to treat any nonzero suffix as something to disregard. That is the property the attack abuses: the suffix is a place you can write anything and a compliant NAT64 router won't care.
The guard's fatal shortcut
Here's the essence of what OpenClaw's guard did: it asked ipaddr.js to classify the address, and for the rfc6052 class it read the trailing two hextets:
case "rfc6145":
case "rfc6052":
// decode IPv4 from the LAST 32 bits - parts[6] and parts[7]
return decodeIpv4FromHextets(parts[6], parts[7]);case "rfc6145":
case "rfc6052":
// decode IPv4 from the LAST 32 bits - parts[6] and parts[7]
return decodeIpv4FromHextets(parts[6], parts[7]);The problem is upstream. ipaddr.js (v2.4.0) classifies two subnets as rfc6052:
rfc6052: [
[new IPv6([0x64, 0xff9b, 0, 0, 0, 0, 0, 0]), 96], // well-known 64:ff9b::/96
[new IPv6([0x64, 0xff9b, 0x1, 0, 0, 0, 0, 0]), 48], // RFC 8215 64:ff9b:1::/48
],rfc6052: [
[new IPv6([0x64, 0xff9b, 0, 0, 0, 0, 0, 0]), 96], // well-known 64:ff9b::/96
[new IPv6([0x64, 0xff9b, 0x1, 0, 0, 0, 0, 0]), 48], // RFC 8215 64:ff9b:1::/48
],- For the /96 prefix, the IPv4 really is in the trailing 32 bits. Reading parts[6] / parts[7] is correct.
- For the /48 prefix, the IPv4 is in bits 48โ63 + 72โ87 (hextets 3, 4, 5). The trailing bits (parts[6] / parts[7]) are the ignored suffix. Reading them is not just wrong; it's reading attacker-controlled scratch space.
Building the lying address
Let's construct the literal that pins 169.254.169.254 (the AWS/GCP-style link-local metadata address) while showing the guard a friendly 8.8.8.8.
- Target (real): 169.254.169.254 โ octets [169, 254, 169, 254] โ hex [0xa9, 0xfe, 0xa9, 0xfe]
- Decoy (shown to guard): 8.8.8.8 โ octets [8, 8, 8, 8] โ hex [0x08, 0x08, 0x08, 0x08]
Now place the target octets in the RFC 6052 /48 slots, and the decoy in the ignored suffix:
hextet[0] = 0x0064 ---> /48 prefix
hextet[1] = 0xff9b ---> /48 prefix
hextet[2] = 0x0001 ---> /48 prefix (this is what makes it the RFC 8215 /48)
hextet[3] = 0xa9fe ---> IPv4 bytes 0,1
hextet[4] = 0x00a9 ---> u-byte(0) : IPv4 byte 2
hextet[5] = 0xfe00 ---> IPv4 byte 3 : (suffix)
hextet[6] = 0x0808 ---> Ignored suffix (guard reads)
hextet[7] = 0x0808 ---> Ignored suffix (guard reads)hextet[0] = 0x0064 ---> /48 prefix
hextet[1] = 0xff9b ---> /48 prefix
hextet[2] = 0x0001 ---> /48 prefix (this is what makes it the RFC 8215 /48)
hextet[3] = 0xa9fe ---> IPv4 bytes 0,1
hextet[4] = 0x00a9 ---> u-byte(0) : IPv4 byte 2
hextet[5] = 0xfe00 ---> IPv4 byte 3 : (suffix)
hextet[6] = 0x0808 ---> Ignored suffix (guard reads)
hextet[7] = 0x0808 ---> Ignored suffix (guard reads)Join them: 64:ff9b:1:a9fe:a9:fe00:808:808
That single literal is the whole exploit. Let's trace what each side sees.
What a compliant NAT64 router sees (decodes per RFC 6052 /48):
- byte 0 = high(0xa9fe) = 0xa9 = 169
- byte 1 = low(0xa9fe) = 0xfe = 254
- byte 2 = low(0x00a9) = 0xa9 = 169 (the 0x00 u-byte is discarded)
- byte 3 = high(0xfe00) = 0xfe = 254
This is 169.254.169.254 (the cloud metadata endpoint).
What the vulnerable guard saw (reads trailing 32 bits, parts[6] / parts[7]):
- 0x0808 = 8.8
- 0x0808 = 8.8
This is 8.8.8.8 (a normal public address).
Why I Find This Beautiful
A missing SSRF check is not difficult to find: a scanner flags it, a reviewer notices the fetch has no guard. But this guard was thorough. It decoded IPv4-mapped, 6to4, Teredo, ISATAP, the well-known NAT64 prefix (a genuinely careful piece of defensive code). It just trusted a library's classification to imply a single memory layout, and that assumption was false for one of the two subnets folded under a shared label.
That's where the interesting vulnerabilities live now: not in the absence of security controls, but in the seams between two systems that almost agree. The guard and the NAT64 router both understand the same address (they just parse it differently), and the attacker lives in the gap. It's a parser-differential bug wearing a high-risk SSRF costume.
And the abused property was a design feature of the standard: RFC 6052 mandating that suffix bits be ignored. That mandate is what created the free scratch space for a decoy. The standard did its job; the implementation just read the wrong half of the address.
Timeline and affected versions
What to do: update your OpenClaw instance to the latest version. That's it. The fix is out and every current release has it, so updating closes this completely.
A little more detail:
- The fix is shipped. It landed in v2026.8.1 and is in every release since. If you're on a recent version, you already have it.
- The bug was there for a stretch of older versions (definitely the beta builds from 2026.7.2-beta.1 to 2026.8.1-beta.2, and possibly some stable versions before that). I'm still working out the exact list with the OpenClaw team, and I'll update this once it's settled.
- You don't need to wait for that list. Whatever version you were on before, updating to the latest fixes it. No other action needed.
Big thanks to the OpenClaw team (they confirmed the high severity SSRF quickly and pushed the fix to everyone). The only thing left to sort out is which old versions to officially mark as affected; the hole itself is already closed for anyone on a current build.
Patched here: https://github.com/openclaw/openclaw/pull/123241
Takeaways for defenders
If you build or review SSRF guards, three things from this report are worth carrying forward:
-
Decode every transition format to its true destination, and confirm the layout (don't infer it from a classification label). A library saying "this is NAT64" doesn't tell you which NAT64 layout, and the byte positions differ by prefix length.
-
Beware ignored / reserved bits. Anywhere a protocol says "these bits don't matter", an attacker will put something that does matter to your parser but doesn't matter to the thing that actually routes.
-
Write tests with adversarial encodings, not just honest ones. A test that only exercises the well-behaved layout will validate a decoder that reads the wrong bytes (because on honest input, the wrong bytes happen to hold the right answer). The crafted-decoy case is the one that tells the truth.
As always, if you'd like to chat more about solving security problems, AI, APIs, or tech in general; hit me up on LinkedIn. My DMs are always open :)