From c31d2f25120546582ab0a69e8eb3f27dfa7b616b Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Sun, 19 Apr 2026 11:53:18 +0000 Subject: [PATCH 1/4] Allow private addresses for explicit URLs in fedify lookup Since v2.1, `fedify lookup` rejected localhost URLs unless `-p`/`--allow-private-address` was passed, because the CLI began forwarding `allowPrivateAddress=false` to the vocab-runtime document loader, whose `validatePublicUrl` check blocks loopback addresses. Split the document/auth loaders into two: - An "initial" loader that always allows private addresses, used for URLs explicitly provided on the command line (plain lookup, the first fetch in `--traverse`, and the first fetch in `--recurse`). - The existing loader, which continues to honor `--allow-private-address`, used for URLs discovered from remote responses (traversal pages and recursion targets). This preserves SSRF protection against `http://localhost/...` URLs embedded in remote `first`/`next`/`inReplyTo` fields while letting users look up local servers without extra flags. Assisted-by: Claude Code:claude-opus-4-7 --- packages/cli/src/lookup.ts | 44 ++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/lookup.ts b/packages/cli/src/lookup.ts index 6389bd322..90280c659 100644 --- a/packages/cli/src/lookup.ts +++ b/packages/cli/src/lookup.ts @@ -83,8 +83,9 @@ const suppressErrorsOption = bindConfig( const allowPrivateAddressOption = bindConfig( flag("-p", "--allow-private-address", { - description: - message`Allow private IP addresses for explicit lookup/traverse requests.`, + description: message`Allow private IP addresses for URLs discovered \ +via traversal or recursion. URLs explicitly provided \ +on the command line always allow private addresses.`, }), { context: configContext, @@ -716,6 +717,18 @@ export async function runLookup( }).start(); let server: TemporaryServer | undefined = undefined; + // URLs explicitly provided by the user always allow private addresses, + // so that local servers can be looked up without -p/--allow-private-address. + // URLs discovered via traversal or recursion follow the option, since they + // originate from remote responses and must be protected against SSRF. + const initialBaseDocumentLoader = await getDocumentLoader({ + userAgent: command.userAgent, + allowPrivateAddress: true, + }); + const initialDocumentLoader = wrapDocumentLoaderWithTimeout( + initialBaseDocumentLoader, + command.timeout, + ); const baseDocumentLoader = await getDocumentLoader({ userAgent: command.userAgent, allowPrivateAddress: command.allowPrivateAddress, @@ -734,6 +747,7 @@ export async function runLookup( ); let authLoader: DocumentLoader | undefined = undefined; + let initialAuthLoader: DocumentLoader | undefined = undefined; let authIdentity: | { keyId: URL; privateKey: CryptoKey } | undefined = undefined; @@ -836,6 +850,24 @@ export async function runLookup( baseAuthLoader, command.timeout, ); + const initialBaseAuthLoader = getAuthenticatedDocumentLoader( + authIdentity, + { + allowPrivateAddress: true, + userAgent: command.userAgent, + specDeterminer: { + determineSpec() { + return command.firstKnock; + }, + rememberSpec() { + }, + }, + }, + ); + initialAuthLoader = wrapDocumentLoaderWithTimeout( + initialBaseAuthLoader, + command.timeout, + ); } spinner.text = `Looking up the ${ @@ -885,8 +917,8 @@ export async function runLookup( command.timeout, ) : undefined; - const initialLookupDocumentLoader: DocumentLoader = authLoader ?? - documentLoader; + const initialLookupDocumentLoader: DocumentLoader = initialAuthLoader ?? + initialDocumentLoader; const recursiveLookupDocumentLoader: DocumentLoader = recursiveAuthLoader ?? recursiveDocumentLoader; let totalObjects = 0; @@ -1109,7 +1141,7 @@ export async function runLookup( let collection: APObject | null = null; try { collection = await effectiveDeps.lookupObject(url, { - documentLoader: authLoader ?? documentLoader, + documentLoader: initialAuthLoader ?? initialDocumentLoader, contextLoader, userAgent: command.userAgent, }); @@ -1248,7 +1280,7 @@ export async function runLookup( for (const url of command.urls) { promises.push( effectiveDeps.lookupObject(url, { - documentLoader: authLoader ?? documentLoader, + documentLoader: initialAuthLoader ?? initialDocumentLoader, contextLoader, userAgent: command.userAgent, }).catch((error) => { From 8cd14c9b1215270077a8aa2e52014a21e78ba5fd Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Sun, 19 Apr 2026 12:15:07 +0000 Subject: [PATCH 2/4] Clarify scope of -p/--allow-private-address in lookup help The option's help text now notes that it only has an effect when combined with -t/--traverse or --recurse, since URLs explicitly provided on the command line always allow private addresses. https://github.com/fedify-dev/fedify/issues/696 https://github.com/fedify-dev/fedify/pull/698 Assisted-by: Claude Code:claude-opus-4-7 --- packages/cli/src/lookup.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lookup.ts b/packages/cli/src/lookup.ts index 90280c659..41605144c 100644 --- a/packages/cli/src/lookup.ts +++ b/packages/cli/src/lookup.ts @@ -84,8 +84,10 @@ const suppressErrorsOption = bindConfig( const allowPrivateAddressOption = bindConfig( flag("-p", "--allow-private-address", { description: message`Allow private IP addresses for URLs discovered \ -via traversal or recursion. URLs explicitly provided \ -on the command line always allow private addresses.`, +via traversal or recursion. This option only has an effect \ +when used together with ${optionNames(["-t", "--traverse"])} \ +or ${optionNames(["--recurse"])}, since URLs explicitly \ +provided on the command line always allow private addresses.`, }), { context: configContext, From 6924183224e699324b5e7ee8c6e5df410c35c3f9 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Sun, 19 Apr 2026 12:15:12 +0000 Subject: [PATCH 3/4] Document fedify lookup private-address behavior Update CHANGES.md with the 2.1.6 entry for the regression fix, and rewrite the -p/--allow-private-address section in docs/cli.md to reflect that URLs given on the command line always allow private addresses while the option gates URLs discovered via --traverse or --recurse. https://github.com/fedify-dev/fedify/issues/696 https://github.com/fedify-dev/fedify/pull/698 Assisted-by: Claude Code:claude-opus-4-7 --- CHANGES.md | 16 ++++++++++++++++ docs/cli.md | 28 +++++++++++++++++++++------- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e2beeafda..d2d5d8c1a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,22 @@ Version 2.1.6 To be released. +### @fedify/cli + + - Fixed `fedify lookup` failing to look up URLs on private or localhost + addresses unless `-p`/`--allow-private-address` was passed, which was a + regression introduced in Fedify 2.1.0 when the CLI began forwarding + the `allowPrivateAddress` option to the underlying document loader. + URLs explicitly provided on the command line now always allow private + addresses, while URLs discovered via [`-t`/`--traverse`] or [`--recurse`] + still honor the option to mitigate SSRF attacks against private + addresses. [[#696], [#698] by Chanhaeng Lee] + +[`-t`/`--traverse`]: https://fedify.dev/cli#t-traverse-traverse-the-collection +[`--recurse`]: https://fedify.dev/cli#recurse-recurse-through-object-relationships +[#696]: https://github.com/fedify-dev/fedify/issues/696 +[#698]: https://github.com/fedify-dev/fedify/pull/698 + Version 2.1.5 ------------- diff --git a/docs/cli.md b/docs/cli.md index 289a810f4..cf9457cdf 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -522,8 +522,10 @@ and `quoteUri` are not accepted as short forms. > are mutually exclusive. > > Recursive fetches always disallow private/localhost addresses for safety. -> `-p`/`--allow-private-address` only applies to explicit lookup/traverse -> targets, not to recursive steps. +> URLs explicitly provided on the command line always allow private +> addresses, while +> [`-p`/`--allow-private-address`](#p-allow-private-address-allow-private-ip-addresses) +> has no effect on recursive steps. ### `--recurse-depth`: Set recursion depth limit @@ -980,18 +982,30 @@ fedify lookup --user-agent MyApp/1.0 @fedify@hollo.social ### `-p`/`--allow-private-address`: Allow private IP addresses -By default, `fedify lookup` does not fetch private or localhost addresses. -The `-p`/`--allow-private-address` option allows explicit lookup/traverse -requests to private addresses when needed for local development. +URLs explicitly provided on the command line always allow private or +localhost addresses, so local servers can be looked up without any extra +flags: ~~~~ sh -fedify lookup --allow-private-address http://localhost:8000/users/alice +fedify lookup http://localhost:8000/users/alice +~~~~ + +The `-p`/`--allow-private-address` option additionally allows private +addresses for URLs discovered via traversal or recursion. It only has an +effect when used together with +[`-t`/`--traverse`](#t-traverse-traverse-the-collection) or +[`--recurse`](#recurse-recurse-through-object-relationships), since URLs +embedded in remote responses are otherwise rejected to mitigate SSRF +attacks against private addresses. + +~~~~ sh +fedify lookup --traverse --allow-private-address http://localhost:8000/users/alice/outbox ~~~~ > [!NOTE] > Recursive fetches enabled by > [`--recurse`](#recurse-recurse-through-object-relationships) continue to -> disallow private addresses. +> disallow private addresses regardless of this option. ### `-s`/`--separator`: Output separator From cff1c31da7751b679f21818315ff3cb1a5c9cad5 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Sun, 19 Apr 2026 12:34:49 +0000 Subject: [PATCH 4/4] Clarify that -p has no effect with --recurse The previous help text, inline comment, changelog entry, and CLI docs all stated that -p/--allow-private-address affects URLs discovered via --recurse, but recursive fetches hardcode `allowPrivateAddress: false` and never honor the option. Update the CLI help, the loader-split comment in `runLookup`, the 2.1.6 changelog entry, and the `-p` section of `docs/cli.md` so that they all describe the same actual behavior: the option only gates URLs discovered during --traverse, while --recurse always disallows private addresses. https://github.com/fedify-dev/fedify/issues/696 https://github.com/fedify-dev/fedify/pull/698 Assisted-by: Claude Code:claude-opus-4-7 --- CHANGES.md | 7 ++++--- docs/cli.md | 9 ++++----- packages/cli/src/lookup.ts | 15 +++++++++------ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d2d5d8c1a..bd1c15f0b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,9 +15,10 @@ To be released. regression introduced in Fedify 2.1.0 when the CLI began forwarding the `allowPrivateAddress` option to the underlying document loader. URLs explicitly provided on the command line now always allow private - addresses, while URLs discovered via [`-t`/`--traverse`] or [`--recurse`] - still honor the option to mitigate SSRF attacks against private - addresses. [[#696], [#698] by Chanhaeng Lee] + addresses, while URLs discovered during [`-t`/`--traverse`] honor the + option to mitigate SSRF attacks against private addresses. Recursive + fetches via [`--recurse`] continue to always disallow private + addresses regardless of the option. [[#696], [#698] by Chanhaeng Lee] [`-t`/`--traverse`]: https://fedify.dev/cli#t-traverse-traverse-the-collection [`--recurse`]: https://fedify.dev/cli#recurse-recurse-through-object-relationships diff --git a/docs/cli.md b/docs/cli.md index cf9457cdf..3a32252b5 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -991,10 +991,9 @@ fedify lookup http://localhost:8000/users/alice ~~~~ The `-p`/`--allow-private-address` option additionally allows private -addresses for URLs discovered via traversal or recursion. It only has an -effect when used together with -[`-t`/`--traverse`](#t-traverse-traverse-the-collection) or -[`--recurse`](#recurse-recurse-through-object-relationships), since URLs +addresses for URLs discovered during traversal. It only has an effect +when used together with +[`-t`/`--traverse`](#t-traverse-traverse-the-collection), since URLs embedded in remote responses are otherwise rejected to mitigate SSRF attacks against private addresses. @@ -1004,7 +1003,7 @@ fedify lookup --traverse --allow-private-address http://localhost:8000/users/ali > [!NOTE] > Recursive fetches enabled by -> [`--recurse`](#recurse-recurse-through-object-relationships) continue to +> [`--recurse`](#recurse-recurse-through-object-relationships) always > disallow private addresses regardless of this option. ### `-s`/`--separator`: Output separator diff --git a/packages/cli/src/lookup.ts b/packages/cli/src/lookup.ts index 41605144c..e4c836fbe 100644 --- a/packages/cli/src/lookup.ts +++ b/packages/cli/src/lookup.ts @@ -84,10 +84,11 @@ const suppressErrorsOption = bindConfig( const allowPrivateAddressOption = bindConfig( flag("-p", "--allow-private-address", { description: message`Allow private IP addresses for URLs discovered \ -via traversal or recursion. This option only has an effect \ -when used together with ${optionNames(["-t", "--traverse"])} \ -or ${optionNames(["--recurse"])}, since URLs explicitly \ -provided on the command line always allow private addresses.`, +during traversal. This option only has an effect when used together \ +with ${optionNames(["-t", "--traverse"])}, since URLs explicitly \ +provided on the command line always allow private addresses and \ +recursive fetches via ${optionNames(["--recurse"])} always disallow \ +them.`, }), { context: configContext, @@ -721,8 +722,10 @@ export async function runLookup( let server: TemporaryServer | undefined = undefined; // URLs explicitly provided by the user always allow private addresses, // so that local servers can be looked up without -p/--allow-private-address. - // URLs discovered via traversal or recursion follow the option, since they - // originate from remote responses and must be protected against SSRF. + // URLs discovered during traversal follow the option to mitigate SSRF + // against private addresses, while recursive fetches always disallow + // private addresses regardless of the option (see the --recurse branch + // below, which hardcodes `allowPrivateAddress: false`). const initialBaseDocumentLoader = await getDocumentLoader({ userAgent: command.userAgent, allowPrivateAddress: true,