Releases: anoni-net/send
Release list
v5.2.0
v5.1.1
Send v5.1.1
One change since v5.1.0:
the origin now sends Cache-Control: no-transform, so a CDN in front of Send
cannot rewrite the responses it serves.
This is the build that powers send.anoni.net.
Nothing about how Send works has changed
The wire protocol, the URL format and the encryption are untouched. Existing
share links keep working. ffsend and other third-party clients keep working.
/__version__ still reports the API generation in version (v3.4.27) and our
release in release (now v5.1.1). This is a server-only change, so the
webpack output and its reproducibility are identical to v5.1.0.
Why this release exists
Running behind Cloudflare, we found that JavaScript Detections (part of Bot
Fight Mode) injects a third-party <script> into the HTML. It reuses the
page's own CSP nonce, so the script runs despite a nonce-based
script-src policy that is supposed to admit only our own scripts.
For a service whose security rests on the served JavaScript being the audited,
reproducible build, that injection is the whole threat: on the key-bearing
download page, code we never wrote and cannot see runs alongside the
decryption. And because the injected loader lives in dynamic HTML, it is not in
SHA256SUMS — byte verification of dist/ still passes while third-party code
runs on the page.
No Cloudflare plan, Pro included, exposes a per-hostname switch for this; the
dashboard toggle is zone-wide. The mechanism that does work per-hostname, on any
plan, is an origin response that carries Cache-Control: no-transform: the CDN
then passes the bytes through untouched and skips the injection.
What changed
- HTML and API responses carry
no-transform, so the injection is skipped
on every page, including the download page and the error pages. - Static assets carry
no-transformtoo, so Cloudflare Polish cannot
re-compress served images. The bytes a visitor receives now matchdist/, and
theSHA256SUMSin this release, even behind a CDN.
If you run Send behind your own CDN or reverse proxy, this is the header to keep
intact. The bundled nginx-send.conf.example in the deployment repo also adds
no-transform on HTML as a second layer.
Verify this release
docker pull ghcr.io/anoni-net/send:5.1.1
cosign verify ghcr.io/anoni-net/send:5.1.1 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/anoni-net/send/.github/workflows/publish.yml@.*$'SHA256SUMS.txt below lists the sha256 of every file the server serves to a
browser, and it is signed:
cosign verify-blob SHA256SUMS.txt \
--bundle SHA256SUMS.txt.cosign.bundle \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/anoni-net/send/.github/workflows/publish.yml@.*$'VERIFYING.md walks
through checking what an instance served you, the image signature, and rebuilding
from source.
Pin by digest in production so the tag cannot move under you:
ghcr.io/anoni-net/send@sha256:c61478d7dbf37d2e7014d13040c892c81de6bae1e9079b195ac21306ef527528
Credit
Send was built by Mozilla and kept alive by
Tim Visée after Firefox Send shut down. This
release stands on both. We are not affiliated with, nor endorsed by, either.
Send v5.1.1(正體中文)
自 v5.1.0 起只有一個
改動:origin 回應現在帶 Cache-Control: no-transform,讓前面的 CDN 不能改寫
Send 送出的回應。
這就是 send.anoni.net 所使用的版本。
Send 的運作方式完全沒有改變
傳輸協定、URL 格式與加密都沒有動過。既有的分享連結繼續可用,ffsend 與其他
第三方客戶端也繼續可用。/__version__ 仍然分開回報 API 世代(version 為
v3.4.27)與我們自己的版號(release,現在是 v5.1.1)。這是純伺服器端的
改動,所以 webpack 產物與它的可重現性都與 v5.1.0 完全相同。
這個版本為什麼存在
在 Cloudflare 後面運作時,我們發現 JavaScript Detections(Bot Fight Mode
的一部分)會把一段第三方 <script> 注入 HTML。它重用頁面自己的 CSP nonce,
所以即使 script-src 是 nonce-based、理應只接受我們自己的 script,這段注入
照樣執行。
對一個安全性建立在「送出的 JavaScript 就是可稽核、可重現的那一份」的服務來說,
這段注入就是核心威脅:在帶金鑰的下載頁上,一段我們從未寫過、也看不到的程式碼
和解密一起執行。而因為注入的載入器活在動態 HTML 裡,它不在 SHA256SUMS 中,
對 dist/ 的逐位元驗證仍會通過,頁面上卻跑著第三方程式碼。
Cloudflare 任何方案(含 Pro)都沒有 per-hostname 的開關可以關掉它,儀表板上的
開關是 zone 層級。真正能做到 per-hostname、且在任何方案都有效的機制,是讓
origin 回應帶 Cache-Control: no-transform:CDN 就會原封不動放行,跳過注入。
改了什麼
- HTML 與 API 回應帶
no-transform,所以每一頁都跳過注入,包含下載頁與
錯誤頁。 - 靜態資源也帶
no-transform,所以 Cloudflare Polish 不能對送出的圖片
再壓縮。訪客收到的位元組現在與dist/、與這個版本的SHA256SUMS一致,即使
在 CDN 後面也是。
如果你把 Send 放在自己的 CDN 或反向代理後面,這是要保持不被改動的標頭。部署
repo 裡的 nginx-send.conf.example 也在 HTML 上補了一層 no-transform。
驗證這個版本
docker pull ghcr.io/anoni-net/send:5.1.1
cosign verify ghcr.io/anoni-net/send:5.1.1 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/anoni-net/send/.github/workflows/publish.yml@.*$'下方的 SHA256SUMS.txt 列出伺服器會送給瀏覽器的每一個檔案的 sha256,而且它有
簽章:
cosign verify-blob SHA256SUMS.txt \
--bundle SHA256SUMS.txt.cosign.bundle \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/anoni-net/send/.github/workflows/publish.yml@.*$'VERIFYING.md 說明如何
檢查某個實例送給你的東西、映像簽章,以及從原始碼重新建置。
正式環境請用 digest 固定版本,這樣 tag 就不會在你不知情的情況下被移動:
ghcr.io/anoni-net/send@sha256:c61478d7dbf37d2e7014d13040c892c81de6bae1e9079b195ac21306ef527528
致謝
Send 由 Mozilla 打造,並在 Firefox Send 關閉之後由
Tim Visée 維持存續。這個版本站在兩者之上。
我們與他們沒有從屬關係,也未獲得他們背書。
v5.1.0
Send v5.1.0
37 commits since v5.0.0.
This release works through a pre-release audit of the whole app: the user-facing
failure modes it found, the accessibility gaps, the dependency backlog, and the
supply chain.
This is the build that powers send.anoni.net.
Nothing about how Send works has changed
The wire protocol, the URL format and the encryption are untouched. Existing
share links keep working. ffsend and other third-party clients keep working.
/__version__ still reports the API generation in version (v3.4.27) and our
release in release (now v5.1.0).
Fixed, and you would have felt these
A blocked WebSocket connection hung the upload forever. new WebSocket()
does not throw when a connection is refused or filtered, so the promise never
settled: the upload tile sat at 0% and the Cancel button did nothing. It now
fails, cancels, or times out, and says which. For anyone on a network that
filters WebSocket upgrades, this is the difference between "your network is
blocking this" and an app that just looks frozen.
Rate limiting had no interface. A 429, whether on upload or download, showed
the generic "Something went wrong" page whose only button invited an immediate
retry that burned another slot. It now explains that you are being asked to wait
and reads the server's Retry-After. The streaming download path also stopped
swallowing every error as a silent reload.
"This link has expired" was shown for a file that had reached its download
limit. Once the record is gone the server genuinely cannot tell the two apart,
so the message no longer claims a cause it does not know.
Unsupported-browser pages pointed people at Firefox, which contradicted the
footer's own "not affiliated with Mozilla or Firefox" and was poor advice for a
Tor Browser user. They are browser-neutral now. An instance served over plain
HTTP also explains that the encryption APIs need a secure origin, rather than
blaming the visitor's browser.
Copy-link no longer reports success it never checked; the JavaScript-disabled
page is styled again; several dialog and copy interactions were fixed.
Accessibility
Dialogs are real dialogs now: a dialog role, a focus trap, Escape to close, and
focus returned where it was. The wrong-password message announces itself to a
screen reader instead of the field silently refocusing. The expiry and
download-count dropdowns, which decide how long a file stays on the server, have
accessible names.
Smaller, and more current
The application bundle is 53 KB gzipped, down from 71 KB, after dropping the
core-js polyfills a WebCrypto-capable browser never uses.
Behind that, a dependency backlog cleared with each dist/-changing step
verified against a real browser: @babel 7 → 8, tailwindcss 2 → 3 (its
built-in engine replaces the separate purge step), stylelint 14 → 17,
@fluent/*, prettier 1 → 3, and the css/postcss loader chain. The Tailwind
upgrade was checked with a 20-shot pixel comparison across screens, light and
dark, desktop and mobile. ws 7 → 8 was verified through a real WebSocket
round-trip.
The production tree keeps shrinking: cldr-core (1.2 MB) is down to the one
52 KB file the app actually reads, and nanohtml's Babel-transform dependencies,
including acorn, are stripped from the image because server-side rendering
never loads them. npm audit --omit=dev reports zero.
Localization
The five fork-added footer strings, previously English-only outside zh-TW and
zh-CN, now cover 33 major languages. Locales without a translation fall back to
English rather than showing a blank.
te and ast were dropped from the served set: below 70% coverage, they showed
the home page and the download-page explanation in English inside an otherwise
translated shell. A node-based locale check runs in CI now, so a broken .ftl or
a served locale with no directory fails the build.
Supply chain
SHA256SUMS.txtis itself signed, so the list that is supposed to catch a
tampered server is no longer protected only by a release permission.- Trivy scans a saved tarball instead of a socket-mounted
:latestimage; every
GitHub Action and the base image are pinned by commit digest, with Dependabot
moving the pins. - Reproducibility is a CI gate: two Node versions build the same commit and the
hashes are compared, instead of the claim resting on a one-time manual check. - The image that is tested, scanned and checksummed is now the exact image that
is signed. This release is the first to run that path.
Verify this release
docker pull ghcr.io/anoni-net/send:5.1.0
cosign verify ghcr.io/anoni-net/send:5.1.0 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/anoni-net/send/.github/workflows/publish.yml@.*$'SHA256SUMS.txt below lists the sha256 of every file the server serves to a
browser, and it is signed:
cosign verify-blob SHA256SUMS.txt \
--bundle SHA256SUMS.txt.cosign.bundle \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/anoni-net/send/.github/workflows/publish.yml@.*$'VERIFYING.md walks
through checking what an instance served you, the image signature, and rebuilding
from source.
Pin by digest in production so the tag cannot move under you:
ghcr.io/anoni-net/send@sha256:39daa1bb1d6e7a0a59441f4ff03529b174dc17cacd41125c78639259480629a6
Known gaps
Tailwind is on 3, not yet 4 (v4's CSS-first rewrite is a separate migration).
Password-protected links still derive their key from the full share URL, so the
same link opened on a different hostname reports the password as wrong: this is
inherited from upstream and matters for anyone running a .onion mirror
alongside a clearnet address. PBKDF2 still uses 100 iterations, bounded because
its salt already contains the URL secret.
Credit
Send was built by Mozilla and kept alive by
Tim Visée after Firefox Send shut down. This
release stands on both. We are not affiliated with, nor endorsed by, either.
Send v5.1.0(正體中文)
自 v5.0.0 起累積 37 個
commit。這個版本把整個 app 走了一遍發布前稽核:找出使用者會遇到的失敗、無障礙
的缺口、相依套件的積欠,以及供應鏈。
這就是 send.anoni.net 所使用的版本。
Send 的運作方式完全沒有改變
傳輸協定、URL 格式與加密都沒有動過。既有的分享連結繼續可用,ffsend 與其他
第三方客戶端也繼續可用。/__version__ 仍然分開回報 API 世代(version 為
v3.4.27)與我們自己的版號(release,現在是 v5.1.0)。
修正,而且你會有感
被阻擋的 WebSocket 連線會讓上傳永遠卡住。 new WebSocket() 在連線被拒或被
過濾時不會丟例外,所以那個 promise 永遠不會 settle:上傳磚停在 0%,取消鈕也
沒有反應。現在它會失敗、取消、或逾時,並說出是哪一種。對在會過濾 WebSocket
升級的網路上的人,這是「你的網路擋住了這個」與「這個網站看起來壞了」的差別。
速率限制沒有介面。 一個 429,不論在上傳或下載,都顯示通用的「Something
went wrong」頁面,而那一頁唯一的按鈕是邀請你立刻重試,重試會再吃掉一個額度。
現在它會說明你被要求稍候,並讀取伺服器的 Retry-After。串流下載路徑也不再把
每一個錯誤吞成無聲的重新載入。
「此連結已過期」被顯示在一個其實是達到下載次數上限的檔案上。 記錄一旦消失,
伺服器真的無法分辨這兩者,所以這個訊息不再宣稱它並不知道的原因。
不支援瀏覽器的頁面把人導向 Firefox,這與頁尾自己的「與 Mozilla 或 Firefox
沒有從屬關係」矛盾,對 Tor Browser 使用者也是錯誤的建議。現在它們是瀏覽器中性
的。以純 HTTP 提供的實例也會說明加密 API 需要安全來源,而不是怪罪訪客的瀏覽器。
複製連結不再回報它從未檢查過的成功;關閉 JavaScript 的頁面重新有了樣式;數個
對話框與複製的互動都修好了。
無障礙
對話框現在是真正的對話框:有 dialog role、焦點困陷、Escape 關閉,以及關閉後把
焦點還回原處。密碼錯誤的訊息會向螢幕報讀軟體播報,而不是欄位無聲地重新取得
焦點。決定檔案在伺服器上保留多久的到期與下載次數下拉,也有了可及名稱。
更小,也更新
應用程式的 bundle 是 53 KB gzip,從 71 KB 降下來,在移除 WebCrypto 瀏覽器
從不使用的 core-js polyfill 之後。
在這背後,一批積欠的相依升級清掉了,每一個會改變 dist/ 的步驟都對真瀏覽器
驗證過:@babel 7 → 8、tailwindcss 2 → 3(內建引擎取代獨立的 purge 步驟)、
stylelint 14 → 17、@fluent/*、prettier 1 → 3,以及 css/postcss 的 loader
鏈。Tailwind 升級用了跨畫面、淺色與深色、桌面與手機的 20 張截圖像素比對來把關。
ws 7 → 8 則透過真實的 WebSocket 往返驗證。
正式相依樹持續縮小:cldr-core(1.2 MB)縮到 app 真正讀取的那一個 52 KB 檔案,
而 nanohtml 的 Babel transform 相依(包含 acorn)從映像中移除,因為伺服器端
算繪從不載入它們。npm audit --omit=dev 回報零。
在地化
五個 fork 新增的頁尾字串,原本在 zh-TW 與 zh-CN 之外只有英文,現在涵蓋 33 個
主要語言。沒有翻譯的語系會退回英文,而不是顯示空白。
te 與 ast 從被服務的語系中移除:覆蓋率低於 70%,它們在其餘已翻譯的外殼裡
把首頁與下載頁的說明顯示成英文。CI 現在會跑一個 node 版的語系檢查,所以一個
壞掉的 .ftl,或一個被服務卻沒有目錄的語系,都會讓建置失敗。
供應鏈
SHA256SUMS.txt本身也被簽章,所以這份「用來抓出被竄改伺服器」的清單,不再
只靠一個 release 權限保護。- Trivy 改掃一個存下來的 tarball,而不是掛載 socket 的
:latest映像;每一個
GitHub Action 與 base image 都用 commit digest 釘住,由 Dependabot 推進。 - 可重現性是一道 CI 閘門:兩個 Node 版本建置同一個 commit,再比對雜湊,而不是
讓這個宣稱只靠一次手動檢查。 - 被測試、被掃描、被 checksum 的映像,現在就是被簽章的那一個。這個版本是第一次
走這條路徑。
驗證這個版本
docker pull ghcr.io/anoni-net/send:5.1.0
cosign verify ghcr.io/anoni-net/send:5.1.0 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/anoni-net/send/.github/workflows/publish.yml@.*$'下方的 SHA256SUMS.txt 列出伺服器會送給瀏覽器的每一個檔案的 sha256,而且它有
簽章:
cosign verify-blob SHA256SUMS.txt \
--bundle SHA256SUMS.txt.cosign.bundle \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/anoni-net/send/.github/workflows/publish.yml@.*$'VERIFYING.md 說明如何
檢查某個實例送給你的東西、映像簽章,以及從原始碼重新建置。
正式環境請用 digest 固定版本,這樣 tag 就不會在你不知情的情況下被移動:
ghcr.io/anoni-net/send@sha256:39daa1bb1d6e7a0a59441f4ff03529b174dc17cacd41125c78639259480629a6
已知的不足
Tailwind 在 3,還不是 4(v4 的 CSS-first 重寫是另一個獨立遷移)。密碼保護的
連結仍然從完整分享網址推導金鑰,所以同一個連結在不同主機名稱下開啟會顯示密碼
錯誤:這是從上游繼承的,對同時經營 .onion 鏡像與 clearnet 位址的人有影響。
PBKDF2 仍使用 100 次迭代,影響有限,因為它的 salt 已經包含網址裡的 secret。
致謝
Send 由 Mozilla 打造,並在 Firefox Send 關閉之後由
Tim Visée 維持存續。這個版本站在兩者之上。
我們與他們沒有從屬關係,也未獲得他們背書。
v5.0.0
Send v5.0.0
41 commits since v4.0.0.
The theme is a smaller trust surface: fewer packages running in your browser,
fewer ways for the server to misbehave, and more of the claims in this
repository checkable rather than asserted.
This is the build that powers send.anoni.net.
Nothing about how Send works has changed
The wire protocol, the URL format and the encryption are untouched. Existing
share links keep working. ffsend and other third-party clients keep working.
/__version__ still reports the API generation in version (v3.4.27) and our
release in release (now v5.0.0).
Files still encrypt in your browser, and the key still never leaves the URL
fragment.
Six server-side defects, found by audit
None of these were reported by a user hitting them. They were found by reading
the code before the release, which is the point of doing it.
Concurrent downloads could exceed the download limit. The count was read,
compared, then incremented, so simultaneous requests for a one-download file all
saw the same value. That matters: for anyone using a one-time link to know
whether a file was fetched twice, the limit is a security property, not a
convenience. Reserving a slot is now a single atomic redis step.
Expired files were never deleted. Redis holds the only expiry TTL. When it
fired, the metadata vanished and the file it pointed at stayed on disk. The FAQ
said files are deleted from the server; now they are. A periodic sweep removes
files whose record is gone.
A crafted WebSocket frame crashed the process. One malformed frame, no
authentication required, stopped the service for everyone. Encrypted metadata
was also unbounded, so a single request could write 100 MiB into redis.
There was no rate limiting at all. Per-IP limits now cover the HTTP API and,
separately and more tightly, the WebSocket upload.
The rate limiter could be switched off by one request header. Introduced by
the fix above and caught in the same audit. The skip for WebSocket upgrades
tested only for the presence of an Upgrade header, so any request carrying one
was served without being counted.
Upload parameters were never type-checked. Every comparison against a
non-numeric value is false, so an expiry of "abc" passed validation and
produced a file that no TTL covered and no sweep would ever match: permanent
storage, obtainable without authentication.
Also removed: an unauthenticated legacy HTTP upload route nothing used, and a
WebSocket fallback address still pointing at Mozilla's shut-down service.
What runs in your browser
The choo framework and its ecosystem are gone, replaced by a small
hand-written framework and event emitter. Fourteen fewer packages reach your
browser. nanohtml and nanomorph are deliberately kept: the first is the
escaping boundary that makes interpolated filenames text instead of markup, and
the second preserves focus while you type a password.
All telemetry and tracking are gone, front and back. Nothing this service
runs reports to anyone else, and the Content-Security-Policy enforces it rather
than promising it.
helmet 3 → 8, with the policy written out in full rather than merged with
library defaults, adding base-uri, script-src-attr, form-action and
object-src. For a service whose security rests on the served JavaScript being
the published JavaScript, the policy that enforces it should be readable in one
place.
The application bundle is 70 KB gzipped. With the locale chunk for your
language, first load is about 74 KB, down from 203 KB at v4.0.0.
Server and build
express 4 → 5. mozlog replaced by a 50-line logger, which unblocks moving
past Node 22 later.
The S3 and Google Cloud SDKs are now optional. They were more than half the
production dependency tree for code a filesystem deployment never loads. 134
packages ship, and npm audit --omit=dev reports zero vulnerabilities in the
image.
The published image now sets NODE_ENV=production. It previously inherited the
development default, which switches off both the CSP and the rate limits, so a
container built from this repository served neither unless its operator knew to
pass the variable.
Verifiability
Lint and both test suites now gate the release itself. They ran on pull
requests but matched neither tag pushes nor main, so the artifact that gets
signed and attested was the one artifact they never ran against.
The release field is read from the source tree alone. It used to fall back
from a dead CircleCI variable. Reaching for the GitHub equivalent would have made
dist/ depend on the build environment and broken the reproducibility guarantee,
so it comes from package.json and a publish-time check requires the git tag to
agree with it.
The build stays reproducible. A clean clone plus npm ci && npm run build
produces dist/ byte-identical to the image, verified for this release across
macOS with Node 26 and Linux with Node 22: all 136 files.
The smoke test now asserts the CSP headers, the rate limit, and that the rate
limit cannot be bypassed by a header, against every image build, alongside the
existing real ffsend encrypted round-trip.
Localization
zh-TW described end-to-end encryption as 點對點加密, the standard rendering of
peer-to-peer. It is corrected to 端對端加密 on the two screens that exist to
explain the guarantee: the home page and every download page.
Verify this release
docker pull ghcr.io/anoni-net/send:5.0.0
cosign verify ghcr.io/anoni-net/send:5.0.0 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/anoni-net/send/.github/workflows/publish.yml@.*$'SHA256SUMS.txt below lists the sha256 of every file the server serves to a
browser, so you can check that what an instance sent you is what we published.
VERIFYING.md walks
through that, the signature, and rebuilding from source.
Pin by digest in production so the tag cannot move under you:
ghcr.io/anoni-net/send@sha256:761dd3bcad90142d5ef2fa687a06be1994bbd7782db196ae3d71bd87ebc6cbae
Known gaps
The pre-release audit found more than was fixed here. What was deliberately
deferred is tracked in
#79, including rate-limit errors
that surface as a generic failure page, a WebSocket connection that hangs
instead of reporting a blocked network, and unsupported-browser pages that still
point at Firefox despite this fork not being affiliated with Mozilla.
Password-protected links derive their key from the full share URL, so the same
link opened on a different hostname reports the password as wrong. This is
inherited from upstream and matters for anyone running a .onion mirror
alongside a clearnet address.
PBKDF2 still uses 100 iterations. The password protects download authorization
only, and its salt already contains the URL secret, so the impact is bounded.
Changing it would change what existing links compute.
Credit
Send was built by Mozilla and kept alive by
Tim Visée after Firefox Send shut down. This
release stands on both. We are not affiliated with, nor endorsed by, either.
Send v5.0.0(正體中文)
自 v4.0.0 起累積 41 個
commit。主軸是縮小信任面:在你瀏覽器裡執行的套件更少、伺服器可能出錯的方式更少,
而這個 repo 宣稱的事情更多是可以查證的,不只是聲明。
這就是 send.anoni.net 所使用的版本。
Send 的運作方式完全沒有改變
傳輸協定、URL 格式與加密都沒有動過。既有的分享連結繼續可用,ffsend 與其他第三方
客戶端也繼續可用。/__version__ 仍然分開回報兩件事,version 是 API 世代
(v3.4.27),release 是我們自己的版號(現在是 v5.0.0)。
檔案依然在你的瀏覽器裡加密,金鑰依然不會離開網址的 fragment。
六個伺服器端缺陷,來自主動稽核
這些全部是在發布前讀程式碼找出來的,沒有一項來自使用者踩到之後的回報。這正是做
稽核的意義。
並發下載可以突破下載次數上限。 原本的作法是先讀取次數、比較、再遞增,所以同時
抵達的請求對一個只允許下載一次的檔案會讀到相同的值。這件事很重要:對任何用一次性
連結來判斷檔案是否被取走兩次的人來說,那個上限是安全性質,不是方便性設定。現在改為
單一個原子的 redis 步驟。
過期的檔案從來沒有被刪除。 redis 持有唯一的過期 TTL。TTL 觸發時 metadata 消失,
但它指向的檔案仍留在磁碟上。FAQ 寫的是檔案會從伺服器刪除,現在才真的如此。定期掃描
會清掉記錄已消失的檔案。
一個構造過的 WebSocket frame 就能讓行程崩潰。 單一個格式錯誤的 frame,不需要
任何認證,就能讓服務對所有人中斷。加密後的 metadata 原本也沒有大小上限,單一個請求
可以往 redis 寫入 100 MiB。
完全沒有速率限制。 現在每個 IP 對 HTTP API 有限制,對 WebSocket 上傳另外有更
嚴格的限制。
速率限制可以被一個請求標頭關掉。 這是上一項的修正帶進來的,在同一次稽核中被
抓到。跳過 WebSocket 升級的判斷只檢查 Upgrade 標頭是否存在,所以任何帶著這個標頭
的請求都會被服務而不計數。
上傳參數從來沒有做型別檢查。 任何與非數字的比較都是 false,所以過期時間送
"abc" 可以通過驗證,並產生一個沒有任何 TTL 覆蓋、也不會被任何掃描匹配到的檔案。
等於未經認證就能取得永久儲存。
另外移除了一個沒有人使用、也不需要認證的舊 HTTP 上傳路徑,以及一個仍指向 Mozilla
已關閉服務的 WebSocket 後備位址。
在你瀏覽器裡執行的東西
choo 框架與它的整套生態已經移除,改用自己寫的小型框架與事件發送器。送到你
瀏覽器的套件少了 14 個。nanohtml 與 nanomorph 是刻意保留的:前者是把插入的
檔名變成文字而非標記的逸出邊界,後者讓你輸入密碼時焦點不會跑掉。
所有追蹤與遙測都已移除,前後端皆然。這個服務執行的任何東西都不會向其他人回報,
而 Content-Security-Policy 是強制執行這件事,不只是承諾。
helmet 3 升到 8,而且政策完整寫出,沒有與程式庫預設值混合,並補上 base-uri、
script-src-attr、form-action 與 object-src。對一個安全性建立在「送出的
JavaScript 就是公開的那份」之上的服務,強制執行這件事的政策應該能在一個地方讀完。
應用程式的 bundle 是 70 KB gzip。加上你語言的語系 chunk,首次載入約 74 KB,v4.0.0
時是 203 KB。
伺服器與建置
express 4 升到 5。mozlog 換成 50 行的 logger,這一步也解開了未來升級到 Node 22
之後的阻礙。
S3 與 Google Cloud 的 SDK 改為選用。它們原本佔正式相依樹一半以上,而檔案系統部署
從來不會載入那些程式碼。現在映像內是 134 個套件,npm audit --omit=dev 回報零個
弱點。
發布的映像現在會設定 NODE_ENV=production。它先前沿用 development 預設值,而那
會同時關掉 CSP 與速率限制,所以從這個 repo 建出來的容器,除非營運者知道要自己傳這
個變數,否則兩者都沒有。
可驗證性
lint 與兩套測試現在會擋住發布本身。 它們原本只在 pull request 上執行,既不匹配
tag 推送也不匹配 main,所以最後被簽章與背書的那個產物,正好是測試套件唯一沒有跑
過的產物。
release 欄位只取自原始碼樹。 它原本會從一個早已失效的 CircleCI 變數退回。改用
GitHub 的對應變數看似直覺,但那會讓 dist/ 取決於建置環境並破壞可重現性保證,所以
它來自 package.json,並由發布時的檢查要求 git tag 與它相符。
建置維持可重現。 乾淨 clone 加上 npm ci && npm run build 產生的 dist/ 與
映像位元一致。這個版本已在 macOS 搭配 Node 26 與 Linux 搭配 Node 22 上驗證,136 個
檔案全部相同。
smoke test 現在會對每一次映像建置斷言 CSP 標頭、速率限制,以及速率限制無法被標頭
繞過,與既有的真實 ffsend 加密往返一起執行。
在地化
zh-TW 把 end-to-end encryption 寫成「點對點加密」,那是 peer-to-peer 的標準中譯。
已更正為「端對端加密」,位置正好是專門用來說明這個保證的兩...
v4.0.0
Send v4.0.0
The first release of Send under anoni.net's
own identity. It is built on upstream timvisee/send
v3.4.27, with 67 commits of modernization on top: current runtime, current
dependencies, our own signed images, and a test suite that runs.
This is the build that powers send.anoni.net.
Nothing about how Send works has changed
The wire protocol, the URL format and the encryption are untouched. Existing
share links keep working. ffsend and other third-party clients keep working.
That is a checkable claim, not a reassurance: app/keychain.js, app/ece.js,
app/api.js, the upload, download and metadata routes, and the HMAC auth
middleware have no commits since v3.4.27. Files still encrypt in your
browser, and the key still never leaves the URL fragment.
Versioning, and why /__version__ says 3.4.27
Our release numbers now diverge from upstream's, so /__version__ reports two
separate things:
{
"version": "v3.4.27",
"release": "v4.0.0",
"source": "https://github.com/anoni-net/send",
"commit": "..."
}version is the API generation a client should speak. Clients gate on it:
ffsend maps >=3.0 <4.0 to its V3 API and has no fallback, so a server
advertising 4.x fails every CLI upload with "unsupported version". Since we
changed no protocol, that field stays where it was.
release is our number. Use it for bug reports, and for telling builds
apart.
source now points at this repository. Previously it advertised upstream's,
which was wrong: the JavaScript we serve is modified, and anyone auditing what
they are running needs the source it was built from. The footer "source" link
moved for the same reason.
Runtime and dependencies
| from | to | |
|---|---|---|
| Node | 16.13 (EOL) | 22 |
| redis client | 3.1.1 | 6.0.0 |
| AWS SDK | aws-sdk 2.x |
@aws-sdk/client-s3 3.x |
| Google Cloud Storage | 6.2.3 | 7.21.0 |
| Sentry (browser) | 5.30.0 | 7.120.3 |
| websocket layer | @dannycoates/express-ws |
mainline express-ws |
| webpack | 4 | 5 |
| ESLint | 6 | 10 (flat config) |
The websocket swap matters more than it looks: the old package loaded its source
through the unmaintained esm package, which crashes outright on Node 22. It
was the thing standing between Send and a current runtime.
Fixed
- Blank page. webpack 5 defaults
output.publicPathtoauto, which
prefixed every asset URL and 404'd the bundle. - Missing logo. SVGs processed by
svgo-loaderexport CommonJS while images
export ES modules, so an asset map that assumed.defaultrendered
src="undefined". - Missing "plus" icon in the upload UI.
- Cancelling a download reported an error. Sentry 7 instruments
fetch, so
an aborted transfer arrived as aTypeErrorrather than the shape the handler
recognised. Cancellation is now recorded as intent instead of inferred from
whatever the error happened to look like.
Security and supply chain
46 of 47 vulnerability alerts resolved: 1 critical, 16 high, 19 medium, 10
low. The remaining one is uuid (medium), pinned in place by teeny-request ^9
upstream; forcing it would violate that range.
Every image is built by our CI and published to ghcr.io/anoni-net/send:
- signed with cosign (keyless, via Sigstore),
- carrying an SBOM and SLSA provenance,
- scanned with Trivy,
- built for linux/amd64 and linux/arm64, each on a native runner rather than
under emulation.
docker pull ghcr.io/anoni-net/send:4.0.0
cosign verify ghcr.io/anoni-net/send:4.0.0 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/anoni-net/send/.github/workflows/publish.yml@.*$'Pin by digest in production so a tag cannot move underneath you. This release is:
ghcr.io/anoni-net/send@sha256:a0f17f9aff65d4014c4fae8d35656e1ad78c68385a36a9bcd9412b167c9bd932
Tests
The test suite had rotted to the point of not running. It runs now, on every
change:
- 56 backend tests, including the first coverage the Google Cloud Storage
backend has ever had. - 21 frontend tests, moved off puppeteer 2 onto Playwright.
- A smoke test against the actual built image: health endpoints, a
/__version__assertion, a page render that fetches the exact asset URLs the
page references, a realffsendencrypted upload and download round-trip with
a checksum comparison, and a headless browser check. - Lint, which had been silently broken for months because nothing ran it.
The smoke test is what caught the version-number problem described above, before
it reached anyone.
Known gaps
uuid(medium) stays open, blocked upstream.- The encrypted round-trip does not run on arm64 in CI, because
ffsend
publishes x86_64 binaries only. Everything else in the smoke test does run
there, and the log says so rather than passing quietly. - The S3 and Google Cloud Storage backends have unit-test and API-surface
coverage, not runtime coverage. Smoke-test them before switching to either. - Trivy reports; it does not yet gate.
Credit
Send was created by Mozilla and kept alive after Mozilla discontinued it by
Tim Visée, whose fork this continues. We are not
affiliated with or endorsed by either. Licensed MPL-2.0, unchanged.
Send v4.0.0(正體中文)
這是 Send 以 anoni.net 自己的身分發布的第一個版本,基於上游
timvisee/send v3.4.27,往上疊了 67 個 commit
的翻修:把執行環境與相依套件帶到現行版本、改用我們自己簽章的映像、讓測試真的跑得起來。
這就是 send.anoni.net 正在跑的版本。
Send 的運作方式完全沒有改變
線上協定、URL 格式與加密方式都沒有動。既有的分享連結繼續有效,ffsend
與其他第三方客戶端也繼續能用。
這是可以查證的陳述:app/keychain.js、app/ece.js、app/api.js、upload
與 download 與 metadata 三個路由、以及 HMAC 驗證中介層,自 v3.4.27
以來沒有任何一個 commit。檔案依然在你的瀏覽器裡加密,金鑰依然只存在於 URL 的
fragment,不會離開。
版號說明,以及 /__version__ 為何仍寫 3.4.27
我們的發行版號從這一版開始與上游分流,所以 /__version__ 分成兩個欄位回報:
{
"version": "v3.4.27",
"release": "v4.0.0",
"source": "https://github.com/anoni-net/send",
"commit": "..."
}version 代表客戶端該使用哪一代 API。客戶端會依這個欄位判斷相容性:ffsend
把 >=3.0 <4.0 對應到它的 V3 API,而且沒有任何 fallback,伺服器若宣告 4.x,
所有 CLI 上傳都會以 unsupported version 失敗。我們既然沒有更動協定,這個欄位就
維持原樣。
release 才是我們自己的版號。回報問題與辨識建置版本時請用它。
source 現在指向本 repo。先前它指向上游,那是錯的:我們送到瀏覽器的 JavaScript
經過修改,想稽核自己正在跑什麼的人,需要看到這份建置實際來自的原始碼。頁尾的
「原始碼」連結基於同樣理由一併調整。
執行環境與相依套件
| 從 | 到 | |
|---|---|---|
| Node | 16.13(已終止支援) | 22 |
| redis client | 3.1.1 | 6.0.0 |
| AWS SDK | aws-sdk 2.x |
@aws-sdk/client-s3 3.x |
| Google Cloud Storage | 6.2.3 | 7.21.0 |
| Sentry(瀏覽器端) | 5.30.0 | 7.120.3 |
| websocket 層 | @dannycoates/express-ws |
主線 express-ws |
| webpack | 4 | 5 |
| ESLint | 6 | 10(flat config) |
websocket 那一項的份量比表面上大:舊套件透過已無人維護的 esm
載入自己的原始碼,而它在 Node 22 上會直接崩潰。它正是擋在 Send
與現行執行環境之間的那道關卡。
修正
- 整頁空白。 webpack 5 把
output.publicPath預設成auto,替每個資源網址加上前綴,導致
bundle 404。 - Logo 顯示不出來。 經過
svgo-loader的 SVG 匯出 CommonJS,圖片則匯出 ES module,
資源對照表若一律假設.default,就會渲染出src="undefined"。 - 上傳介面缺少「加號」圖示。
- 取消下載會顯示錯誤。 Sentry 7 會插樁
fetch,中止的傳輸因此以TypeError
的形式出現,與處理程式原本比對的錯誤型態不符。現在改成記錄「使用者要取消」這個意圖,
不再從錯誤長相反推。
安全性與供應鏈
47 個弱點通報已修掉 46 個:1 個 critical、16 個 high、19 個 medium、10 個 low。
剩下的 1 個是 uuid(medium),被上游 teeny-request ^9 的版本範圍卡住,強行升版會違反該範圍。
每個映像都由我們的 CI 建置並發布到 ghcr.io/anoni-net/send:
- 以 cosign 簽章(keyless,透過 Sigstore)
- 附帶 SBOM 與 SLSA provenance
- 經 Trivy 掃描
- 提供 linux/amd64 與 linux/arm64,兩者都在原生機器上建置,不使用模擬
docker pull ghcr.io/anoni-net/send:4.0.0
cosign verify ghcr.io/anoni-net/send:4.0.0 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/anoni-net/send/.github/workflows/publish.yml@.*$'正式環境請以 digest 釘選,避免標籤在你腳下被移動。本次發行的 digest:
ghcr.io/anoni-net/send@sha256:a0f17f9aff65d4014c4fae8d35656e1ad78c68385a36a9bcd9412b167c9bd932
測試
這套測試原本已經荒廢到跑不起來。現在每次變更都會跑:
- 56 個後端測試,其中包含 Google Cloud Storage 後端有史以來第一次的測試覆蓋。
- 21 個前端測試,從 puppeteer 2 移轉到 Playwright。
- 針對實際建置出來的映像做 smoke test:健康檢查端點、
/__version__斷言、
抓取頁面實際引用的資源網址來確認渲染、用真的ffsend跑一次加密上傳與下載往返並比對
checksum,以及一次 headless 瀏覽器檢查。 - Lint,它先前因為沒有任何地方會執行而靜靜壞了好幾個月。
上面提到的版號問題,正是這個 smoke test 在影響到任何人之前攔下來的。
已知的不足
uuid(medium)仍未關閉,卡在上游。- CI 的 arm64 不會跑加密往返測試,因為
ffsend只發布 x86_64 的執行檔。smoke test
其餘項目在 arm64 上照跑,而且會在記錄中明確寫出跳過了哪一段,不會靜靜地過關。 - S3 與 Google Cloud Storage 後端目前只有單元測試與 API 介面層級的驗證,沒有實際執行的覆蓋。
要改用這兩者之前,請先自行實測一次。 - Trivy 目前只回報,還沒有設成硬性關卡。
致謝
Send 由 Mozilla 開發,在 Mozilla 停止服務後由 Tim Visée
接手維護,本專案延續的正是他的分支。我們與兩者皆無隸屬關係,也未受其背書。
授權維持 MPL-2.0 不變。