Skip to content

Commit fe5d177

Browse files
committed
Merge tag '1.10.9' into 2.0-maintenance
Fedify 1.10.9
2 parents 1c2d303 + b6997f6 commit fe5d177

7 files changed

Lines changed: 141 additions & 49 deletions

File tree

CHANGES.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ Version 2.0.16
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.0.15
1319
--------------
@@ -908,6 +914,18 @@ Released on February 22, 2026.
908914
[#351]: https://github.com/fedify-dev/fedify/issues/351
909915

910916

917+
Version 1.10.9
918+
--------------
919+
920+
Released on May 10, 2026.
921+
922+
### @fedify/fedify
923+
924+
- Fixed `validatePublicUrl()` allowing private IPv4 addresses encoded as
925+
IPv4-mapped IPv6 URL literals, such as `http://[::ffff:7f00:1]/`, which
926+
could bypass private network protections in remote document loading.
927+
928+
911929
Version 1.10.8
912930
--------------
913931

@@ -1131,6 +1149,18 @@ Released on December 24, 2025.
11311149
- Implemented `list()` method in `WorkersKvStore`. [[#498], [#500]]
11321150

11331151

1152+
Version 1.9.10
1153+
--------------
1154+
1155+
Released on May 10, 2026.
1156+
1157+
### @fedify/fedify
1158+
1159+
- Fixed `validatePublicUrl()` allowing private IPv4 addresses encoded as
1160+
IPv4-mapped IPv6 URL literals, such as `http://[::ffff:7f00:1]/`, which
1161+
could bypass private network protections in remote document loading.
1162+
1163+
11341164
Version 1.9.9
11351165
-------------
11361166

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
redis.disconnect();
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
redis.disconnect();
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
redis.disconnect();
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
redis.disconnect();
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
redis.disconnect();
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
redis.disconnect();
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.

0 commit comments

Comments
 (0)