* fix(web): escape untrusted mesh fields in dashboard to close DOM XSS
Security review found user-controlled mesh data reaching innerHTML unescaped
(upstream-originating; worth a PR back to l5yth/potato-mesh):
- High: channel_name was interpolated raw by formatChatChannelTag and rendered
via innerHTML on the node-detail page and map overlays, so a crafted channel
name (any radio can set one) executed script for every viewer. Now escaped
with the canonical escapeHtml.
- Defense-in-depth: node_id, role, and hw_model in the node table are now
escaped for consistency with sibling cells.
Regression test added for the channel-name escape. Full JS suite: 1448 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(web): gate POST /api/instances by federation mode + hardening
Security review findings in the Sinatra app (upstream-originating; worth a PR
back to l5yth/potato-mesh):
- High: POST /api/instances performed outbound federation fetches and DB writes
with no federation/private guard, so a PRIVATE=1 or FEDERATION=0 node still
acted on unsolicited signed announcements. Added `halt 404 unless
federation_enabled?`, mirroring the existing GET /api/instances guard.
- Low: the JSON-LD <script> block now escapes </script> breakout characters.
- Low: federation peer fetches now enforce a max response size
(REMOTE_INSTANCE_MAX_RESPONSE_BYTES, default 8 MiB) to bound memory.
Regression spec added for the federation-gate (private + FEDERATION=0 cases).
NOTE: rspec could not be run locally (this host has only Ruby 2.6; the project
needs >=3.0 and no Docker was available). Changes are `ruby -c` syntax-checked
and reviewed against the mirrored guard; the ruby.yml CI workflow must confirm
the spec on push/PR before merge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(matrix): stop silent message loss + add HTTP timeouts + log reg failures
Security/resilience review of the Matrix bridge (upstream-originating; worth a
PR back to l5yth/potato-mesh):
- High: within a poll batch, a later message that forwarded successfully
advanced the in-memory rx_time watermark past an EARLIER message that failed,
so the failed message was never refetched -> silent permanent loss. poll_once
now breaks at the first failure, committing only the contiguous successful
prefix; the failed message and everything after it are retried in order next
poll (no reordering, no duplicates). Regression test added and proven to bite.
- High: the shared reqwest client had no timeouts, so a hung homeserver/API
stalled the single-threaded poll loop (and startup health checks) forever.
Added timeout(30s) + connect_timeout(10s).
- Medium: ensure_user_registered swallowed all non-success responses silently;
it now warns (status + body) on anything other than the expected
already-registered case, so a bad as_token is diagnosable.
cargo test: 118 passed; cargo fmt clean. Tradeoff: a permanently-failing
message now blocks its batch (retried each poll) rather than being lost -- safer
than silent loss; a skip-after-N/dead-letter follow-up is possible if needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: address Copilot review on the security PR (Matrix errcode + spec hygiene)
- matrix.rs: ensure_user_registered now treats only the specific M_USER_IN_USE
errcode as "already registered", instead of any 400 Bad Request, so real 400
failures (malformed request, config issues) reach the diagnostic warn branch.
Updated the M_USER_IN_USE test to send the errcode body and added a test that
a non-M_USER_IN_USE 400 is not suppressed. cargo test: 119 passing.
- app_spec.rb: the private-mode POST /api/instances example now restores
ENV["PRIVATE"] via an around/ensure block (mirroring the FEDERATION pattern),
so private mode can't leak into later specs and cause order-dependent failures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: implement Copilot follow-ups — streaming size cap + bounded skip
- instance_fetcher.rb: perform_single_http_request now uses the block form of
Net::HTTP#request + response.read_body to enforce the response-size cap
INCREMENTALLY, aborting as soon as the limit is exceeded, instead of letting
the non-block form buffer the whole body into memory first. Updated the two
federation_spec mocks to the block/streaming form and added a size-cap test.
- main.rs (Matrix bridge): a permanently-failing ("poison") message no longer
blocks the batch forever. Consecutive per-message failures are counted
in-memory; after MAX_FORWARD_ATTEMPTS (5) the message is skipped (advanced
past, with a warning) so later messages make progress. Transient failures
still stop-and-retry in order (no silent loss, no reordering). Added a
skip-after-N regression test; the original watermark test still passes.
cargo test: 120 passing, cargo fmt clean.
Ruby: instance_fetcher.rb is `ruby -c` clean; the spec exercises the new paths
but rspec/`ruby -c` can't run locally (Ruby 2.6 here can't parse the repo's
3.1+ syntax) — the ruby.yml CI is the gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(web): fix private-mode /api/instances spec to actually enable private mode
Running the suite locally (Ruby 4.0) revealed the private-mode POST /api/instances
example never entered private mode: the top-level `before` hook deletes
ENV["PRIVATE"] before each example, so toggling it (even via an around block)
had no effect and the request returned 201 instead of 404.
Switch to stubbing PotatoMesh::Config.private_mode_enabled? => true, the pattern
the rest of the suite uses for private-mode cases. This makes the test actually
exercise the guard AND removes the ENV manipulation entirely (so there is no
cross-spec ENV leak to worry about). Full web suite: 1470 examples, 0 failures;
rufo clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore: add guardrails
* docs: fix meshcore badge
* web: life chat message cap at 1000 in frontend
* web: honor protocol in chat
* web: deprioritize test channels
* fix ci
Backfill v0.6.1 CHANGELOG entry (previously undocumented),
add v0.6.2 entry covering 9 commits since v0.6.1, and bump
the version string across data, web, matrix, and app along
with the README docker-pull examples.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump version to 0.6.0 and remove deprecated env var aliases
BREAKING CHANGES:
- POTATOMESH_INSTANCE removed — use INSTANCE_DOMAIN
- PROVIDER removed — use PROTOCOL
- MESH_SERIAL removed — use CONNECTION
- PORT config alias removed — use CONNECTION
The _ConfigModule proxy class (which kept PROTOCOL/PROVIDER and
CONNECTION/PORT in sync) is deleted. docker-compose.yml now defaults
INSTANCE_DOMAIN to http://web:41447 so deployments without an explicit
value continue to work.
* tests: run black
* address review comments
* matrix: listen for synapse on port 41448
* matrix: address review comments
* matrix: address review comments
* matrix: cover missing unit test vectors
* matrix: cover missing unit test vectors
* matrix: cache seen messages by rx_time not id
* matrix: fix review comments
* matrix: fix review comments
* matrix: cover missing unit test vectors
* matrix: fix tests
* matrix: add health checks to startup
* matrix: address review comments
* matrix: cover missing unit test vectors
* matrix: cover missing unit test vectors
* matrix: create potato-matrix-bridge
* matrix: add unit tests
* matrix: address review comments
* ci: condition github actions to only run on paths affected...
* Add comprehensive unit tests for config, matrix, potatomesh, and main modules
* Revert "Add comprehensive unit tests for config, matrix, potatomesh, and main modules"
This reverts commit 212522b4a2.
* matrix: add unit tests
* matrix: add unit tests
* matrix: add unit tests