| CVE | Vulnerability name | Date | Responsible Security Disclosure by | Vulnerabilities |
|---|---|---|---|---|
|
GHSA-c5xr-mg26-vq5w
|
TransitBleed |
2026-08-03 |
tonghuaroot and xet7![]() Coordinated disclosure via GitHub Security Advisory GHSA-c5xr-mg26-vq5w. |
|
Wherever WeKan fetches a URL somebody else chose — an attachment URL, an outgoing
webhook, an avatar, every import path — the address it is about to contact is
checked against one block-list, isIpBlocked() in
models/lib/attachmentUrlValidation.js. It is deliberately the only one:
the input-time validator and the delivery-time guard fetchSafe() both call it,
so the two cannot drift apart. That was itself the fix for
DnsBleed.
Its IPv6 half decided from the spelling of the address:
// vulnerable — string prefixes decide
if (normalized.startsWith('::ffff:')) {
return isIpv4Blocked(normalized.replace('::ffff:', ''));
}
if (normalized.startsWith('2001:db8')) return true;
const firstValue = parseInt(normalized.split(':')[0], 16);
if (firstValue >= 0xfc00 && firstValue <= 0xfdff) return true; // fc00::/7
if (firstValue >= 0xfe80 && firstValue <= 0xfebf) return true; // fe80::/10
if (firstValue >= 0xff00) return true; // ff00::/8
return false;
IPv6 has several standard ways to write “this packet goes to an IPv4 address”,
and none of them looks like ::ffff::
| Transition mechanism | Prefix | Where the IPv4 sits | Example wrapping 169.254.169.254 |
|---|---|---|---|
| 6to4 (RFC 3056) | 2002::/16 |
bits 16–48 | 2002:a9fe:a9fe:: |
| NAT64 well-known prefix (RFC 6052) | 64:ff9b::/96 |
low 32 bits | 64:ff9b::a9fe:a9fe |
| Teredo (RFC 4380) | 2001:0000::/32 |
low 32 bits, as the bitwise complement | 2001:0:5ef5:79fd:0:0:5601:5601 |
On infrastructure with a 6to4 relay or a NAT64 gateway — common in cloud and
Kubernetes networks — the packet really does arrive at the embedded IPv4 address. So
http://[2002:a9fe:a9fe::]/latest/meta-data/ read the cloud metadata endpoint
through the guard whose whole job was to stop it, and
http://[2002:7f00:0001::]/, http://[2002:0a00:0001::]/ and
http://[64:ff9b::c0a8:101]/ reached 127.0.0.1,
10.0.0.1 and 192.168.1.1. Because both halves of the defence
share this one function, both the URL a user submits and the request WeKan actually sends
were affected.
Deciding from the spelling failed in a plainer way too: 0:0:0:0:0:ffff:7f00:1
is an ordinary IPv4-mapped 127.0.0.1, merely written out instead of
compressed, and startsWith('::ffff:') does not match it.
Fix: expand the address to its 16 bytes once, and read those bytes. Notation then cannot change the answer — compressed or not, dotted-quad tail or not, with an interface zone or without. Every form that embeds an IPv4 address has it extracted and re-checked with the IPv4 rules, so if the embedded IPv4 is blocked, its IPv6 spelling is blocked too.
// fixed — models/lib/attachmentUrlValidation.js
function isIpv6Blocked(ip) {
const bytes = ipv6ToBytes(ip);
if (!bytes) return true; // unparseable → fail closed
if (embeddedIpv4Addresses(bytes).some(address => isIpv4Blocked(address))) return true;
if (bytes[0] === 0x20 && bytes[1] === 0x01
&& bytes[2] === 0x0d && bytes[3] === 0xb8) return true; // 2001:db8::/32
if (bytes[0] === 0xfc || bytes[0] === 0xfd) return true; // fc00::/7
if (bytes[0] === 0xfe && (bytes[1] & 0xc0) === 0x80) return true; // fe80::/10
if (bytes[0] === 0xfe && (bytes[1] & 0xc0) === 0xc0) return true; // fec0::/10
if (bytes[0] === 0xff) return true; // ff00::/8
return false;
}
The forms extracted are 6to4, NAT64 (the well-known prefix and the RFC 8215 local-use one,
the latter at every RFC 6052 embedding position because only the gateway knows which it
uses), Teredo through both its server address and its obfuscated client address,
IPv4-mapped, IPv4-translated, IPv4-compatible, and ISATAP under any routing prefix rather
than only the link-local one the reporter's list stops at. The deprecated
fec0::/10 site-local range is blocked as well. Anything that is not a valid
IPv6 address is blocked, as before.
A transition address wrapping a public IPv4 is still allowed —
2002:5db8:d822:: and 64:ff9b::93.184.216.34 both go through
— and tests/transitbleed.test.cjs pins that as carefully as it pins the
bypasses, because a guard that blocks everything is a guard somebody switches off.
Reported privately by tonghuaroot via GitHub Security Advisory GHSA-c5xr-mg26-vq5w. Fixed at the upcoming WeKan release.