Skip to content

Commit f18d2cf

Browse files
committed
Merge tag '2.1.12' into 2.2-maintenance
Fedify 2.1.12
2 parents 6388127 + 914da16 commit f18d2cf

7 files changed

Lines changed: 131 additions & 32 deletions

File tree

CHANGES.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ Version 2.2.1
88

99
To be released.
1010

11+
### @fedify/vocab-runtime
12+
13+
- Fixed `validatePublicUrl()` allowing private IPv4 addresses encoded as
14+
IPv4-mapped IPv6 URL literals, such as `http://[::ffff:7f00:1]/`, which
15+
could bypass private network protections in remote document loading.
16+
1117

1218
Version 2.2.0
1319
-------------
@@ -282,6 +288,18 @@ Released on April 28, 2026.
282288
[#722]: https://github.com/fedify-dev/fedify/pull/722
283289

284290

291+
Version 2.1.12
292+
--------------
293+
294+
Released on May 10, 2026.
295+
296+
### @fedify/vocab-runtime
297+
298+
- Fixed `validatePublicUrl()` allowing private IPv4 addresses encoded as
299+
IPv4-mapped IPv6 URL literals, such as `http://[::ffff:7f00:1]/`, which
300+
could bypass private network protections in remote document loading.
301+
302+
285303
Version 2.1.11
286304
--------------
287305

@@ -729,6 +747,18 @@ Released on March 24, 2026.
729747
[#599]: https://github.com/fedify-dev/fedify/pull/599
730748

731749

750+
Version 2.0.16
751+
--------------
752+
753+
Released on May 10, 2026.
754+
755+
### @fedify/vocab-runtime
756+
757+
- Fixed `validatePublicUrl()` allowing private IPv4 addresses encoded as
758+
IPv4-mapped IPv6 URL literals, such as `http://[::ffff:7f00:1]/`, which
759+
could bypass private network protections in remote document loading.
760+
761+
732762
Version 2.0.15
733763
--------------
734764

@@ -1606,6 +1636,18 @@ Released on February 22, 2026.
16061636
[#351]: https://github.com/fedify-dev/fedify/issues/351
16071637

16081638

1639+
Version 1.10.9
1640+
--------------
1641+
1642+
Released on May 10, 2026.
1643+
1644+
### @fedify/fedify
1645+
1646+
- Fixed `validatePublicUrl()` allowing private IPv4 addresses encoded as
1647+
IPv4-mapped IPv6 URL literals, such as `http://[::ffff:7f00:1]/`, which
1648+
could bypass private network protections in remote document loading.
1649+
1650+
16091651
Version 1.10.8
16101652
--------------
16111653

@@ -1829,6 +1871,18 @@ Released on December 24, 2025.
18291871
- Implemented `list()` method in `WorkersKvStore`. [[#498], [#500]]
18301872

18311873

1874+
Version 1.9.10
1875+
--------------
1876+
1877+
Released on May 10, 2026.
1878+
1879+
### @fedify/fedify
1880+
1881+
- Fixed `validatePublicUrl()` allowing private IPv4 addresses encoded as
1882+
IPv4-mapped IPv6 URL literals, such as `http://[::ffff:7f00:1]/`, which
1883+
could bypass private network protections in remote document loading.
1884+
1885+
18321886
Version 1.9.9
18331887
-------------
18341888

packages/redis/src/kv.test.ts

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,50 +7,83 @@ import process from "node:process";
77
const redisUrl = process.env.REDIS_URL;
88
const ignore = redisUrl == null;
99

10-
function getRedis(): { redis: Redis; keyPrefix: string; store: RedisKvStore } {
10+
async function cleanupPrefixedKeys(
11+
redis: Redis,
12+
keyPrefix: string,
13+
): Promise<void> {
14+
let cursor = "0";
15+
do {
16+
const [nextCursor, keys] = await redis.scan(
17+
cursor,
18+
"MATCH",
19+
`${keyPrefix}*`,
20+
"COUNT",
21+
"100",
22+
);
23+
cursor = nextCursor;
24+
if (keys.length > 0) {
25+
await redis.del(...keys);
26+
}
27+
} while (cursor !== "0");
28+
}
29+
30+
function getRedis(): {
31+
redis: Redis;
32+
keyPrefix: string;
33+
store: RedisKvStore;
34+
cleanup: () => Promise<void>;
35+
} {
1136
const redis = new Redis(redisUrl!);
1237
const keyPrefix = `fedify_test_${crypto.randomUUID()}::`;
1338
const store = new RedisKvStore(redis, { keyPrefix });
14-
return { redis, keyPrefix, store };
39+
return {
40+
redis,
41+
keyPrefix,
42+
store,
43+
cleanup: () => cleanupPrefixedKeys(redis, keyPrefix),
44+
};
1545
}
1646

1747
test("RedisKvStore.get()", { ignore }, async () => {
1848
if (ignore) return; // see https://github.com/oven-sh/bun/issues/19412
19-
const { redis, keyPrefix, store } = getRedis();
49+
const { redis, keyPrefix, store, cleanup } = getRedis();
2050
try {
2151
await redis.set(`${keyPrefix}foo::bar`, '"foobar"');
2252
assert.strictEqual(await store.get(["foo", "bar"]), "foobar");
2353
} finally {
54+
await cleanup();
2455
await redis.quit();
2556
}
2657
});
2758

2859
test("RedisKvStore.set()", { ignore }, async () => {
2960
if (ignore) return; // see https://github.com/oven-sh/bun/issues/19412
30-
const { redis, keyPrefix, store } = getRedis();
61+
const { redis, keyPrefix, store, cleanup } = getRedis();
3162
try {
3263
await store.set(["foo", "baz"], "baz");
3364
assert.strictEqual(await redis.get(`${keyPrefix}foo::baz`), '"baz"');
3465
} finally {
66+
await cleanup();
3567
await redis.quit();
3668
}
3769
});
3870

3971
test("RedisKvStore.delete()", { ignore }, async () => {
4072
if (ignore) return; // see https://github.com/oven-sh/bun/issues/19412
41-
const { redis, keyPrefix, store } = getRedis();
73+
const { redis, keyPrefix, store, cleanup } = getRedis();
4274
try {
4375
await redis.set(`${keyPrefix}foo::baz`, '"baz"');
4476
await store.delete(["foo", "baz"]);
4577
assert.equal(await redis.exists(`${keyPrefix}foo::baz`), 0);
4678
} finally {
79+
await cleanup();
4780
await redis.quit();
4881
}
4982
});
5083

5184
test("RedisKvStore.list()", { ignore }, async () => {
5285
if (ignore) return; // see https://github.com/oven-sh/bun/issues/19412
53-
const { redis, store } = getRedis();
86+
const { redis, store, cleanup } = getRedis();
5487
try {
5588
await store.set(["prefix", "a"], "value-a");
5689
await store.set(["prefix", "b"], "value-b");
@@ -67,14 +100,14 @@ test("RedisKvStore.list()", { ignore }, async () => {
67100
assert(entries.some((e) => e.key[1] === "b"));
68101
assert(entries.some((e) => e.key[1] === "nested"));
69102
} finally {
70-
await redis.flushdb();
103+
await cleanup();
71104
await redis.quit();
72105
}
73106
});
74107

75108
test("RedisKvStore.list() - single element key", { ignore }, async () => {
76109
if (ignore) return; // see https://github.com/oven-sh/bun/issues/19412
77-
const { redis, store } = getRedis();
110+
const { redis, store, cleanup } = getRedis();
78111
try {
79112
await store.set(["a"], "value-a");
80113
await store.set(["b"], "value-b");
@@ -87,14 +120,14 @@ test("RedisKvStore.list() - single element key", { ignore }, async () => {
87120
assert.strictEqual(entries.length, 1);
88121
assert.strictEqual(entries[0].value, "value-a");
89122
} finally {
90-
await redis.flushdb();
123+
await cleanup();
91124
await redis.quit();
92125
}
93126
});
94127

95128
test("RedisKvStore.list() - empty prefix", { ignore }, async () => {
96129
if (ignore) return; // see https://github.com/oven-sh/bun/issues/19412
97-
const { redis, store } = getRedis();
130+
const { redis, store, cleanup } = getRedis();
98131
try {
99132
await store.set(["a"], "value-a");
100133
await store.set(["b", "c"], "value-bc");
@@ -107,7 +140,7 @@ test("RedisKvStore.list() - empty prefix", { ignore }, async () => {
107140

108141
assert.strictEqual(entries.length, 3);
109142
} finally {
110-
await redis.flushdb();
143+
await cleanup();
111144
await redis.quit();
112145
}
113146
});

packages/vocab-runtime/src/url.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ test("validatePublicUrl()", async () => {
1919
await rejects(() => validatePublicUrl("https://localhost"), UrlError);
2020
await rejects(() => validatePublicUrl("https://127.0.0.1"), UrlError);
2121
await rejects(() => validatePublicUrl("https://[::1]"), UrlError);
22+
await rejects(
23+
() => validatePublicUrl("http://[::ffff:7f00:1]/"),
24+
UrlError,
25+
);
26+
await validatePublicUrl("https://[2001:db8::1]");
2227
});
2328

2429
test("isValidPublicIPv4Address()", () => {
@@ -37,6 +42,7 @@ test("isValidPublicIPv6Address()", () => {
3742
ok(!isValidPublicIPv6Address("fe80::1")); // link-local
3843
ok(!isValidPublicIPv6Address("ff00::1")); // multicast
3944
ok(!isValidPublicIPv6Address("::")); // unspecified
45+
ok(!isValidPublicIPv6Address("::ffff:7f00:1")); // IPv4-mapped
4046
});
4147

4248
test("expandIPv6Address()", () => {

packages/vocab-runtime/src/url.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@ export async function validatePublicUrl(url: string): Promise<void> {
1919
}
2020
let hostname = parsed.hostname;
2121
if (hostname.startsWith("[") && hostname.endsWith("]")) {
22-
hostname = hostname.substring(1, hostname.length - 2);
22+
hostname = hostname.slice(1, -1);
2323
}
2424
if (hostname === "localhost") {
2525
throw new UrlError("Localhost is not allowed");
2626
}
27+
const hostnameFamily = isIP(hostname);
28+
if (hostnameFamily !== 0) {
29+
validatePublicIpAddress(hostname, hostnameFamily);
30+
return;
31+
}
2732
if ("Deno" in globalThis && !isIP(hostname)) {
2833
// If the `net` permission is not granted, we can't resolve the hostname.
2934
// However, we can safely assume that it cannot gain access to private
@@ -50,14 +55,18 @@ export async function validatePublicUrl(url: string): Promise<void> {
5055
addresses = [];
5156
}
5257
for (const { address, family } of addresses) {
53-
if (
54-
family === 4 && !isValidPublicIPv4Address(address) ||
55-
family === 6 && !isValidPublicIPv6Address(address) ||
56-
family < 4 || family === 5 || family > 6
57-
) {
58-
throw new UrlError(`Invalid or private address: ${address}`);
59-
}
58+
validatePublicIpAddress(address, family);
59+
}
60+
}
61+
62+
function validatePublicIpAddress(address: string, family: number): void {
63+
if (
64+
family === 4 && isValidPublicIPv4Address(address) ||
65+
family === 6 && isValidPublicIPv6Address(address)
66+
) {
67+
return;
6068
}
69+
throw new UrlError(`Invalid or private address: ${address}`);
6170
}
6271

6372
export function isValidPublicIPv4Address(address: string): boolean {

patches/vitepress@1.6.3.patch

Lines changed: 0 additions & 13 deletions
This file was deleted.

pnpm-lock.yaml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ packages:
4444
- examples/solidstart
4545
- examples/sveltekit-sample
4646

47+
allowBuilds:
48+
'@tailwindcss/oxide': true
49+
esbuild: true
50+
protobufjs: true
51+
sharp: true
52+
unrs-resolver: true
53+
workerd: true
54+
4755
catalog:
4856
"@cloudflare/workers-types": ^4.20250906.0
4957
"@fxts/core": ^1.20.0

0 commit comments

Comments
 (0)