mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-07 16:11:46 +02:00
The authoring workflow told the reader to curl two URLs an unauthenticated
MCP endpoint chooses: resource_metadata out of the WWW-Authenticate
challenge, and the issuer out of the document that URL returns. A hostile
endpoint could point either at 169.254.169.254, at a loopback service, or
at a public hostname whose DNS record is 127.0.0.1, and a bare curl would
go there with the author's network position.
references/safe-discovery.md adds safe_curl, and step 2 now uses it for
both fetches. It is an allowlist, not a denylist, and it fails closed:
https only, no userinfo, port 443 only, and every resolved address checked
against loopback, private, CGNAT, link-local, unique-local, multicast,
benchmarking, documentation and reserved space. IPv4-mapped, NAT64 and 6to4
forms are unwrapped and the embedded IPv4 address is checked under the IPv4
rules. One bad answer in a round-robin set refuses the whole host.
Two details that decide whether this works rather than looks like it does.
The fetch pins --resolve to exactly the addresses that were validated, so a
second DNS answer cannot move the request between the check and the
connection. And no redirect is ever followed: a 3xx is reported and
refused, and going there means re-running safe_curl on the Location so it
is validated on its own merits.
Executed against adversarial metadata rather than a URL list: a hostile
challenge and two hostile protected-resource documents, parsed the way step
2 says to parse them. localtest.me is the case a string check cannot catch
-- an ordinary public hostname with a real public record pointing at
loopback. Twenty refusal cases, one per class; the redirect refusal against
a live 301; and the documented flow still returns Enterpret's 401 challenge
and protected-resource document unchanged. The two scripts in the document
were extracted and diffed against the tested copies: byte-identical.
Also the four Enterpret acceptance findings that belong in this package:
- Every pinned count in catalog-contract.md was stale. APP_STORE_DEFINITIONS
went 46 -> 56 and SELF_SERVE_MCP_CANDIDATES 43 -> 48 since the file was
written, so following it wrote a failing assertion. The counts are now a
command that reads them out of the checkout in front of you.
- Every line-number citation in both skills is gone, in favour of a file
plus a symbol to grep. Four playbook citations landed on unrelated text;
every claim behind them survives and was re-read at 18dac1e1, which both
SKILL.md files now record.
- The visibility contract has three states, not two. Hiding a slug is not
withholding it -- a hidden slug is still reachable by direct URL or slug
lookup. availability: { available: false, reason } is the state that
refuses setup, and it is where a connector authored without a live
provider proof belongs.
- Installed catalog skills materialize as connect-agent-tools--<hash>, so
the three bare-slug sibling paths were dead for any installed reader.
They now name the skill, with the command to resolve the directory.
skills-catalog: manifest regenerated (19 skills), validate clean, 20 tests.
Both packages still derive markdown_only; descriptions 291 and 293. Server
company-skills suites 161/161 against the regenerated manifest. Seventeen
packaged relative links, none broken, no private references.
Co-Authored-By: Paperclip <noreply@paperclip.ing>