Skip to content

Require policy-approved URLs for outbound requests in Fedify 3.0 #1087

Description

@dahlia

ActivityPub references lead to requests for objects, contexts, and inboxes. Today, validatePublicUrl() returns Promise<void>, so a new request path can omit validation without a type error. I propose an AllowedUrl branded type for Fedify 3.0, required by the functions that perform outbound HTTP requests.

An AllowedUrl means that a particular request policy approved that exact URL. It does not establish the remote object's authenticity or make DNS results permanently safe. Vocabulary IDs and other stored references would remain ordinary URLs.

Preserve runtime URL compatibility

Use a URL subclass for approved values, with a branded, readonly public type. Only the approval factory should construct it. All URL setters should throw TypeError; overriding a setter must preserve its getter too.

searchParams should return one cached ImmutableUrlSearchParams instance, built from a detached copy of the approved query. Its append(), delete(), set(), and sort() methods should throw.

Illustrative API: client owns the request policy; method names are provisional.

const reference = new URL("https://example.org/actors/alice");
await client.fetch(reference); // Type error: URL is not AllowedUrl.

const allowed = await client.allowUrl(reference);
await client.fetch(allowed);

allowed.searchParams.set("page", "2"); // Throws TypeError.
const url: URL = allowed;
url.pathname = "/other"; // Throws TypeError despite the ordinary URL type.

Subclass overrides can be bypassed by calling native prototype setters or the native searchParams getter directly. Keep the approved href and issuing policy in private state, check both at the request boundary, and construct the request from that recorded string. A URL approved under allowPrivateAddress must not carry that approval into a stricter client. Copies and serialized values lose approval; queue consumers must approve destinations again under their current policy.

Own the document loader interface

Change Fedify's DocumentLoader to accept URL, with an adapter for jsonld.js's string-based interface. Keep document resolution separate from network fetching: the loader checks preloaded contexts and caches first, then obtains an AllowedUrl if it needs the network. The network fetcher requires AllowedUrl.

type DocumentLoader = (
  url: URL,
  options?: DocumentLoaderOptions,
) => Promise<RemoteDocument>;

type DocumentFetcher = (
  url: AllowedUrl,
  options?: DocumentLoaderOptions,
) => Promise<RemoteDocument>;

Approve only when the loader needs the network, so offline contexts and local identifiers avoid network-policy checks. Bind the policy when creating the loader so recursive loads use the same policy. Offer a network-fetcher extension point that receives approved URLs; replacing the whole resolver still gives application code responsibility for any requests it makes.

Do not automatically normalize JSON-LD identifiers as part of this change. The adapter must preserve the input IRI string where existing identity comparisons or cache keys use it. Whether RemoteDocument.documentUrl and contextUrl should also become URLs needs separate review; a fetched document's approval does not approve its context reference.

Cover every actual request

Redirect destinations and references discovered through HTML or Link headers need their own approval. Signed requests and retries must use the same request boundary; converting an AllowedUrl to Request must not discard the check. Verify the final request URL against the approved destination, re-sign redirected requests, and define which credentials may cross origins. Restrict native fetch() calls in remote-data handling code to the audited request implementation.

Request-time DNS checks remain necessary. Separate DNS validation followed by portable fetch() still leaves a validation-to-connection race; connection pinning or egress controls need a separate runtime-specific design.

Prototype the subclass and loader adapter before settling the public API. Check readonly type behavior, native-setter bypass detection, policy isolation, queue restoration, offline contexts, and signed redirect/retry paths on Deno, Node.js, Bun, and Cloudflare Workers. The API changes target 3.0; internal request consolidation can begin in 2.x.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Fields

    Priority

    None yet

    Effort

    None yet

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions