diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-01 11:47:39 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-01 11:47:39 +0200 |
| commit | 760ac421b1944cd69a80e3a92127a1a966f15938 (patch) | |
| tree | c894b655d4f21698ebb91c0865ddf3e04985586d /docs | |
| parent | 752b160c7c5e671e0db8f402a52fac27bb85ab06 (diff) | |
| download | meshbay-760ac421b1944cd69a80e3a92127a1a966f15938.tar.gz | |
fix: downloads are marked and keep their extension; Explorer files are refused
The desktop app writes the Mark-of-the-Web on each file it saves on Windows, as
a browser does. Bidirectional controls are reserved characters in a saved name
(portable-name.js and paths.sanitize_for_download, and again in the main
process), so a name cannot display one extension and carry another. The node
refuses uploads of files Windows Explorer acts on by itself: desktop.ini,
.lnk, .url, .scf, .library-ms, .searchConnector-ms (F-19).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 47 |
1 files changed, 36 insertions, 11 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 1f2e43d..a43e35d 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -1621,7 +1621,11 @@ reason to show progress. Five protections, and they are the substance: -- a **filename allowlist**; +- a **filename allowlist**, and **no file Windows Explorer acts on by itself** — + `desktop.ini`, `.lnk`, `.url`, `.scf`, `.library-ms`, `.searchConnector-ms`. Shown in + a folder the operator browses, any of them can make Explorer contact a server + of the uploader's choosing with the operator's Windows credentials; refused on + every node, since a Linux node's folder may be shared to Windows; - **no overwrite** — a colliding name gets a free one. The check is `Path.exists()`, and `stat()` is itself case-insensitive on NTFS and exFAT, so this already holds there. A name an upload in flight will take counts as taken, since its file @@ -2324,15 +2328,29 @@ The rules that make this safe: CONFLICT DO UPDATE … WHERE … RETURNING` — so a concurrent burst gets no more attempts than the limit. A request that checked no passphrase gives its attempt back. -- **Every path that checks the passphrase counts on the same row**: sign-in, - passphrase change, changing the e-mail address on file and account deletion. A - right passphrase clears it; failures older than the window age out. +- **A browser the account signed in from has a row of its own.** A successful + sign-in from a browser that presented no token is answered with one (`known_browser`, + a random value the hub keeps only hashed, at most twenty per account, the least + recently used going first); a later sign-in presenting it is counted on its own + row, which nobody else can spend. The username's row, which anyone can spend, + then locks only browsers the account has never used. The token is not a + credential — the passphrase is still checked, at the same rate — and a token + for another account counts on the name's row like no token at all (**M1**). It + survives sign-out by design, and a passphrase reset or the account's erasure + forgets every one. +- **A passphrase re-checked inside an open session counts on the session's + account row** — passphrase change, changing the e-mail address on file, account + deletion, registering a device, asking for the bundle pepper. A stranger + failing at sign-in does not stop the owner doing any of them, and failures there + lock nothing at sign-in. A right passphrase clears the row it was checked on; + failures older than the window age out. - **A lockout refuses passphrase sign-in and nothing else.** Open sessions, token - renewal and device sign-in continue, and a reset code sent to the address on - file clears it — so a stranger who locks a public username costs its owner at - most a new sign-in (**AV26**). A session learns its own lockout from - `/v1/users/me`, because a passphrase change re-wraps every node's bundle before - the hub accepts the new passphrase and must not start when the hub would refuse. + renewal and device sign-in continue, a known browser signs in on its own row, + and a reset code sent to the address on file clears it — so a stranger who + locks a public username costs its owner at most a sign-in from a new browser + (**AV26**). A session learns its own row's lockout from `/v1/users/me`, because + a passphrase change re-wraps every node's bundle before the hub accepts the new + passphrase and must not start when the hub would refuse. --- @@ -2433,6 +2451,11 @@ What running it establishes, and what each fact costs: - **OS-backed secret storage is real on a desktop and honest without one.** With a keyring it is keyring-backed; headless, the same code reports unavailable and **refuses to store rather than downgrading silently**. +- **A downloaded file carries the Mark-of-the-Web**, as a browser's download + does: on Windows the application writes `Zone.Identifier` (ZoneId 3) beside each + file it saves, so SmartScreen and Protected View apply when it is opened. The + application writes its files itself, so nothing else would mark them; FAT and + exFAT have no such stream. - **Installation places files, never secrets.** No key generation in a package's post-install step or an installer custom action — a golden image would give every machine the same key. @@ -3218,7 +3241,9 @@ requirements, not compatibility notes. the name its disk gave the file and never rewrites it — that is the string that opens it — so a single file, a zip's entries and the zip's own name are passed through `portable-name.js` at the moment of saving (reserved characters become -`_`, a trailing dot or space goes, a reserved stem gains `_`), and the transfer's +`_` — the bidirectional controls among them, since `invoice\u202efdp.exe` displays +as `invoiceexe.pdf` — a trailing dot or space goes, a reserved stem gains `_`), and +the transfer's row names the original when it changed. The rule is `paths.sanitize_for_download`, and the two are held byte-identical by a parity test. Two different names can still become one — a zip keeps both entries under it. @@ -3561,7 +3586,7 @@ had already been asked. | **AV23** | **An upload's owner is recorded when the upload ends and applied when the entry is created**, which are different moments (§5.4). Written against the index at the end of the upload it matched nothing, every time, and left every uploaded file owned by nobody — so no member could delete what they had sent | | **AV24** | **A node registered for no group is refused signaling, not exempted from it** (§7.2). The membership check was written as "if the node claims any group", so it skipped itself — membership, group status and the public-group gate together — for the node AV1 made commonplace: the unconfigured one, which is also the one least able to absorb the work | | **AV25** | **Which nodes host a group is answered to its members** (§7.3). Only the public case checked, so a private group told any authenticated account that knew its id which machines hosted it — and an ex-member knows that id for ever | -| **AV26** | **A sign-in lockout refuses passphrase sign-in and nothing else** (§7.7). It is keyed by username, usernames are public, and so anyone can spend somebody else's attempts. Open sessions, renewal and device sign-in are untouched and a reset code ends it, which bounds what a stranger buys to one forced sign-in. The lockout is a DoS primitive by construction; this is the ceiling on it | +| **AV26** | **A sign-in lockout refuses passphrase sign-in and nothing else** (§7.7). Its username row is public, so anyone can spend it; a browser the account has signed in from counts on a row of its own, and passphrase checks inside a session on the account's. Open sessions, renewal and device sign-in are untouched and a reset code ends it, which bounds what a stranger buys to a sign-in from a browser the account never used | | **AV27** | **A free-text third-party search is bounded per member and per node** (§6.5). `tmdb_search_req` spends the *operator's* credential, which TMDB rates and the whole group's automatic matching depends on, so one member holding a search box degrades the library for everyone. Per member and not per connection — three tabs is one person — and kept in the group context so a reconnect does not reset it. The refusal is an error, because an empty result list is what "no such film" looks like | | **AV29** | **An invitation link is bounded on both halves and its mail on the sender** (§3.4, §7.3). Twenty outstanding per group on the node (bearer codes) and on the hub (tickets); and because a link mail reaches an address the hub has no relationship with, at the request of anyone who owns a group, it is counted **per sending account per day** (`mail.invite_link_daily_cap`, 10), under the recipient and instance bounds and outside the recovery reserve (`invite_link` is not a recovery purpose) | | **AV28** | **How many node keys one account may announce is bounded** (§7.2). Each is a row plus an IP-log row under a one-year retention, so an account in a loop writes a year of storage on the operator's disk having paid only for signatures. Proof of possession (**M8**) settles whose key it is and not how many. Counted only where a row is added: re-announcing a key already held keeps working at the ceiling, or a node that reached it could never refresh its address again | |