<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-node/src/meshbay_node/linkpreview.py, branch 0.16</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.16</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.16'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-09-01T17:09:20Z</updated>
<entry>
<title>fix(node): bound and tighten the chat link-preview SSRF surface</title>
<updated>2026-09-01T17:09:20Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-01T17:09:20Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=0808d594a371e7caea54f45a71db586160ae75ad'/>
<id>urn:sha1:0808d594a371e7caea54f45a71db586160ae75ad</id>
<content type='text'>
The link-preview fetch is an outbound request to an address a member
chose. safe_url() already blocked non-public addresses and re-checked
each redirect hop; this adds the parts that were missing:

- Rate limit. `_do_link_preview_request` was reachable by any member
  with no ceiling, so a member — or a hub minting tokens for many
  accounts — could drive unbounded outbound HTTP from the operator's
  machine (amplification / DoS / on-demand IP disclosure to arbitrary
  hosts). Now bounded per connection (15) and node-wide (60) over a
  60 s window; only a real fetch counts, a cache hit is free, and over
  the ceiling the reply is a plain `ok: false` (bare link), not cached.

- Port allowlist. safe_url() passed `parts.port` straight through, so
  a member could aim the node at `http://&lt;public-host&gt;:&lt;any-port&gt;`.
  Restricted to {80, 443, 8080, 8443} — every real OpenGraph page,
  none of SSH / mail / DB / cache / search / admin ports.

- DNS rebinding. The connection's actual peer address is now
  re-checked against the public-address rule (`_reject_if_rebound`),
  so a name that resolves clean and then to something internal does
  not get its body read. Best-effort (no `network_stream` extension,
  no check); a full literal-pin is noted as remaining hardening.

- Decompression bomb. `_downscale` now refuses an image whose header
  dimensions exceed ~40 MP before convert()/thumbnail() decode it.

Third security review, finding M3.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_011pG75yGK3NthNfyjH74omG
</content>
</entry>
<entry>
<title>feat(chat): link previews for pasted URLs</title>
<updated>2026-08-28T01:43:19Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-28T01:43:19Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=ce4e10c4b8bd9c66c375c3a5d5c18d8552655775'/>
<id>urn:sha1:ce4e10c4b8bd9c66c375c3a5d5c18d8552655775</id>
<content type='text'>
Paste an http(s) link in a group's chat and it unfurls into an OpenGraph
card — title, description, site name, and image — the way WhatsApp/Signal/
Slack do it.

The fetch is the node's, never the browser's or the hub's. The browser
cannot: a strict img-src/connect-src and CORS block it, and a direct fetch
would leak every reader's IP to the linked host on each render. The hub must
not touch group content (draft-v6 §2.5). The node already fetches third-party
metadata for the Videos and Music apps, over the same authorised path.

Flow mirrors media_meta_req: the client sends `link_preview_req {url}`, the
node replies `link_preview_resp` with the card fields (or `ok: false`), and
any OG image is stored under its blake3 in the existing media_cache thumb
store — the client then fetches it via the normal file_req path, exactly like
a poster. Nothing durable is added: the card text lives in a bounded in-memory
TTL cache on the node (draft-v6 §2.7 — enrichment on demand, the asking device
caches), and MNP goes 0.11 → 0.12 (additive: an older node logs "unknown type"
and the client shows the bare link).

Because the URL is chosen by a *member* and triggers an outbound request from
the operator's machine, `linkpreview.safe_url` is an SSRF gate: http(s) only,
no credentials, and every resolved address must be globally routable — no
loopback, private, link-local, multicast or reserved range, cloud-metadata
included. Redirects are followed by hand so each hop is re-checked. Residual,
documented in the module: DNS rebinding between the check and connect, closed
properly by pinning the checked IP — a follow-up.

Also fixes a long-standing chat annoyance the preview cards made worse:
opening the Chat tab landed a screen or two above the newest message because
the scroll-to-bottom ran before attachment thumbnails and (now) preview cards
had loaded and grown the content. A ResizeObserver keeps the view pinned to
the bottom through late content growth, and does nothing once the reader
scrolls up.

Tests: test_linkpreview.py (the SSRF gate and the OpenGraph parse, incl.
redirect re-validation and image downscaling) and test_link_preview_request.py
(reply shape, the media_cache image round-trip, the result cache).

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
</content>
</entry>
</feed>
