<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-node/src/meshbay_node/linkpreview.py, branch 0.17</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.17</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.17'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-10-01T07:49:58Z</updated>
<entry>
<title>fix(node): link previews connect to the address they checked, without blocking</title>
<updated>2026-10-01T07:49:58Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-10-01T07:49:58Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=6426912946270bb02e7b94508008edf8f949d993'/>
<id>urn:sha1:6426912946270bb02e7b94508008edf8f949d993</id>
<content type='text'>
The name is resolved off the event loop and every answer checked; the socket
is then opened to that IP literal through a pinned httpcore backend, TLS still
verifying the certificate for the name, and no proxy from the environment.
A name that answers clean and then with a LAN address no longer gets a
request sent there, and a slow name no longer stalls the node (F-12).

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix: a member can no longer lock a node, crash it with a link, or stop hub cleanup</title>
<updated>2026-09-30T09:22:24Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-30T09:22:24Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=69554fac7eba6eef7eb8a1c0111c5b92e7f21256'/>
<id>urn:sha1:69554fac7eba6eef7eb8a1c0111c5b92e7f21256</id>
<content type='text'>
- node: only a wrong code counts towards the join lock, now per account
  (5) as well as node-wide (20), and it is consulted only when a code is
  tried. Every member reconnecting gets the group key through join_request,
  so a lock checked before recognition let one member refuse it to everyone.
- node: link previews read the body as a stream and stop at the cap,
  counted on decoded bytes; a declared oversized image is not read; 15 s
  total deadline; image decoding off the loop. `client.get` had buffered
  the whole (decompressed) response before the caps looked at it.
- hub: the daily purge of never-verified accounts detaches their IP-log
  rows (keeping the name) and clears every other reference first, and each
  cleanup step runs on its own. On PostgreSQL the bare DELETE violated the
  ip_logs foreign key and stopped every purge behind it for good.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<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>
