Skip to content

Security vi

SkimMail docs edited this page Sep 18, 2026 · 8 revisions

English · Tiếng Việt · 中文

Security

Trang này nói cụ thể về năm cơ chế — claim code lần đầu, screen lock, trusted proxy, đăng nhập OAuth, và encryption at rest — và cố tình thẳng thắn về những gì mỗi cơ chế không che chắn. Muốn xem toàn bộ tư thế bảo mật (header, rate-limit, audit log, chặn SSRF), xem SECURITY.md trong repo; muốn báo lỗ hổng bảo mật, làm theo hướng dẫn trong file đó thay vì mở issue công khai.

Mô hình mối đe doạ, nói thẳng. Các cơ chế này chống lại kẻ tấn công tiếp cận instance của bạn qua mạng, hoặc lấy được một bản sao file của bạn — ổ đĩa bị lấy, một bản backup, một DATA_DIR bị lộ. Chúng không chống lại kẻ đã có quyền root trên máy trong lúc máy đang chạy: kẻ đó đọc được process memory, kể cả unlock secret đang được dùng.

Claim code

Trước khi có ai đăng nhập, màn hình đăng nhập của SkimMail nếu không có gì chặn sẽ tạo credential cho bất kỳ ai tới trước — passphrase hay tài khoản đầu tiên được tạo bởi request đầu tiên hỏi tới nó. Trên một mạng có quy mô bất kỳ, đó là một cuộc đua giữa bạn và bất kỳ ai khác có thể chạm tới port đó, trong khoảng thời gian từ "container đã khởi động" tới "bạn ngồi xuống và mở trình duyệt".

Từ 1.9.0, một instance mới in ra một claim code dùng một lần (SKIM-XXXX-XXXX-XXXX) vào log ở lần khởi động đầu tiên và từ chối tạo credential đầu tiên nếu thiếu nó. Chỉ hash SHA-256 của code được lưu lại — không lưu plaintext — nên một khi đã in ra, bản ghi duy nhất còn lại là bất cứ thứ gì đã bắt được dòng log đó. Điều này cũng có nghĩa SkimMail không bao giờ hiện lại code đó cho bạn được nữa: không có tính năng "hiện lại", cố tình như vậy, và claim-code --show chỉ tồn tại để giải thích điều đó và trỏ bạn sang --rotate.

Đọc code (xem Installation để biết lệnh chính xác theo từng cách cài):

sudo journalctl -u skimmail | grep 'claim code'          # apt — bắt buộc sudo
docker compose logs skimmail 2>&1 | grep 'claim code'    # Docker Compose — container không tên là skimmail
docker logs skimmail 2>&1 | grep 'claim code'            # docker run --name skimmail

Trước 1.17.0, trang này (và màn hình first-run của wizard) chỉ hiện dạng docker logs. Lệnh đó hỏng dưới Compose: file docker-compose.yml được publish đặt tên project là skimmail mà không có container_name, nên container thật là skimmail-skimmail-1, không phải skimmail. docker compose logs <service> tra theo tên service trong file compose bất kể tên container thật là gì, đó là lý do nó là dạng đúng cho một bản triển khai Compose. Đã sửa ở 1.17.0 (SKIMMAIL-190). Xem Installation để biết giải thích đầy đủ và lệnh chính xác cho từng cách cài.

Mất code, hoặc log đã xoay vòng qua mất rồi? Tạo một code mới — code cũ ngừng dùng được ngay khi bạn làm việc này:

sudo -u skimmail skimmail --data-dir /var/lib/skimmail claim-code --rotate

Restore một backup lên một instance quay lại trạng thái setup lần đầu (một bản không có credential nào, hoặc restore lên một target hoàn toàn mới) sẽ tự động tạo và in ra một code mới — một code đã dùng hết không bao giờ được xem là "đã claim" theo kiểu một bản release cũ từng làm, điều mà nếu còn thì sẽ để bất kỳ ai tạo tài khoản admin trong khoảng thời gian đó.

Trên một mạng thật sự kín — nơi không ai không đáng tin có thể chạm tới port trước bạn — bạn có thể bỏ qua hẳn cổng này bằng SKIMMAIL_SKIP_CLAIM=1. Làm điều này một cách có chủ đích: nó gỡ bỏ biện pháp kiểm soát duy nhất đứng giữa "container đã khởi động" và "instance thuộc về bất kỳ ai hỏi trước".

Screen lock

Screen lock khi rảnh (idle) là một ranh giới truy cập thật, không phải một lớp vẽ đè lên app. Trước 1.9.0, lock được vẽ hoàn toàn ở trình duyệt: session bên dưới vẫn hợp lệ hoàn toàn suốt thời gian đó, nên API vẫn trả lời bình thường, và reload tab đang khoá làm lock biến mất trong khi dữ liệu vẫn còn trên màn hình.

Từ 1.9.0, khoá một session là việc server làm. Một session đang khoá:

  • vẫn giữ danh tính (vẫn là bạn, vẫn đang đăng nhập) nhưng mất khả năng với tới: mọi route API trả về 423 Locked, trừ vài route mà chính màn hình khoá cần dùng (/api/auth/unlock, /api/auth/logout, /api/auth/me, /api/server-info);
  • sống sót qua một lần reload — không có state phía client để mất, vì chính server là thứ đang thực thi việc khoá;
  • không làm ngừng thư của bạn. Sync nền vẫn chạy và thư mới vẫn tới trong khi session đang khoá — lock là ranh giới cho người ngồi trước bàn phím, không phải nút tạm dừng cho service.

Unlock sẽ xác thực lại passcode hiện tại của bạn (và mã 2FA, nếu bạn đã bật two-factor) qua cùng cơ chế chống brute-force như lúc đăng nhập — nó không tạo session mới, nên thời hạn của lần đăng nhập gốc không đổi. Lock không hề được cung cấp dưới AUTH_MODE=none: không có credential để khoá đằng sau, thì cũng chẳng có gì để unlock.

Hai setting điều khiển việc này, đều ở Settings ▸ Security ▸ Timeouts và đều thuộc Plane 2 (xem Configuration):

Setting Mặc định từ environment Ý nghĩa
Auto-lock after AUTO_LOCK_MINUTES (0 = không bao giờ) Số phút không hoạt động trước khi client tự khoá
Session expires after SESSION_TTL_HOURS (720 = 30 ngày) Một lần đăng nhập kéo dài bao lâu trước khi phải đăng nhập lại

Từ 1.16.2, lock giữ nguyên trên mọi tab đang mở, không chỉ tab kích hoạt nó. Lock luôn nằm ở hàng session, nên nó vốn đã dùng chung — nhưng trước 1.16.2, việc một tab cụ thể có vẽ lớp phủ hay không lại là state React riêng của từng tab, nên một tab thứ hai mở cùng session vẫn hiện nguyên một hộp thư đã render đầy đủ, không hề có lớp phủ. Điều đó phá gần hết lý do idle lock tồn tại. Các tab giờ trao đổi một tín hiệu qua localStorage mỗi khi khoá/mở khoá, và cố tình xử lý hai chiều không đối xứng — chính sự không đối xứng này mới là tính chất bảo mật, không phải chi tiết cài đặt:

  • một tín hiệu locked vẽ lớp phủ ở mọi tab khác ngay lập tức — fail-closed, nên điều tệ nhất một tín hiệu giả tạo ra được là hiện màn hình nhập passcode cho một người rồi vẫn phải gõ đúng passcode thật;
  • một tín hiệu unlocked không bao giờ tự gỡ lớp phủ. Nó chỉ khiến tab đó hỏi lại server (/api/auth/me), và lớp phủ chỉ gỡ khi chính câu trả lời của server nói session đã mở khoá.

Thiếu sự không đối xứng đó, một script cùng-origin có thể gỡ màn khoá chỉ bằng cách ghi một key localStorage — tái lập đúng khiếm khuyết mà 1.9.0 đã sửa, hồi lớp khoá còn là một lớp phủ vẽ lên trên một workspace không bao giờ unmount.

Cũng từ 1.16.2: một lời từ chối không còn bị ghi lại thành kết quả rỗng. Trước bản này, một trang được reload trong lúc đang khoá sẽ hỏi danh sách hộp thư, bị từ chối với 423 vì session đang khoá, và ghi lời từ chối đó thành "bạn chưa có hộp thư nào" — vẽ ra màn hình lần-đầu "Your first mailbox" bên dưới lớp phủ khoá, trên một instance vẫn luôn có thư suốt thời gian đó. Gõ passcode xong lại lộ ra màn hình đó thay vì hộp thư của bạn; chỉ reload toàn trang mới khôi phục được. Cùng một loại lỗi này còn xuất hiện ở: một lượt tải thư thất bại vẽ ra "Inbox Zero", một danh sách folder thất bại thì biến mất thay vì báo lỗi, và danh sách phiên đăng nhập ở trên có thể nói "không có phiên nào đang hoạt động" trong khi đang đọc từ chính bên trong một phiên như vậy. Không cái nào trong số này là bản vá riêng cho lock — một 423 từ bất kỳ session đang khoá nào giờ luôn đi tới lớp phủ khoá thay vì một màn lỗi, "đang tải" và "rỗng" không còn là cùng một trạng thái nữa, và chỉ một 401 thật sự mới được coi là đã đăng xuất. Xem Xử lý sự cố để biết đúng triệu chứng mà bản vá này thay thế.

Trusted proxy

X-Forwarded-For là một header mà client bắt đầu viết và mỗi proxy đứng trước instance của bạn nối thêm vào — nên địa chỉ client viết luôn nằm ở vị trí trái nhất, còn địa chỉ mà proxy gần bạn nhất thực sự quan sát được nằm ở vị trí phải nhất. SkimMail đọc header này từ phải sang trái, dừng lại ở hop đầu tiên không được công nhận là proxy của chính bạn. Điều này quan trọng vì client có thể viết bất cứ gì vào header đó — một phiên bản code cũ hơn từng tin vào entry trái nhất, nghĩa là kẻ tấn công có thể đặt một X-Forwarded-For mới ở mỗi request và rơi vào một rate-limit bucket mới mỗi lần, vô hiệu hoá cả login lockout lẫn per-IP limiter, và ghi vào audit log bất kỳ địa chỉ nào chúng muốn.

Hai setting điều khiển việc này:

  • TRUST_PROXY (Plane 1, trust_proxy) — mặc định tắt. Khi tắt, X-Forwarded-For bị bỏ qua hoàn toàn và địa chỉ TCP peer trực tiếp chính là địa chỉ của client, hết.
  • TRUSTED_PROXIES — danh sách CIDR (cách nhau bằng dấu phẩy) gồm các hop được tính là hạ tầng của bạn chứ không phải client. Để trống, các địa chỉ loopback và private được coi là proxy (một sidecar hay một nginx cùng host), đúng cho trường hợp phổ biến và không sai cho trường hợp nào khác.

Hậu quả của việc cấu hình sai chạy theo cả hai chiều. Bật TRUST_PROXY chỉ có ý nghĩa khi bản thân SkimMail không thể chạm tới trực tiếp bởi bất kỳ ai ngoài reverse proxy thật của bạn — code không có cách nào phân biệt "kết nối này đi qua proxy của tôi" với "kết nối này là kẻ lạ giả làm proxy của tôi", ngoại trừ kiểm tra nó thực sự đến từ đâu. Cụ thể:

  • Nếu reverse proxy của bạn đứng ở một địa chỉ public (một CDN edge, một tunnel exit) và bạn để TRUSTED_PROXIES trống, SkimMail sẽ không nhận ra nó là proxy chút nào, và địa chỉ được dùng để rate-limit, lockout và audit log sẽ sai.
  • Nếu port của SkimMail có thể chạm tới bởi bất kỳ ai khác ngoài proxy của bạn — hoặc nếu bạn đặt TRUSTED_PROXIES quá rộng — một kẻ tấn công chạm được thẳng vào port đó có thể tự viết X-Forwarded-For của riêng chúng và khiến SkimMail tin vào bất kỳ địa chỉ nào chúng chọn, đúng là lỗ hổng giả mạo mà cách đọc-từ-phải-sang-trái đã vá cho trường hợp mặc định.

Quy tắc gói trong một câu: chỉ bật TRUST_PROXY khi port không thể chạm tới bằng cách nào khác ngoài qua proxy thật của bạn, và đặt TRUSTED_PROXIES đúng bằng địa chỉ thật của proxy đó bất cứ khi nào nó không phải sẵn là loopback hay một dải private.

Đăng nhập OAuth

Từ 1.18.0, cả hai cách hoàn tất đăng nhập Google hoặc Microsoft — redirect thông thường và flow dán tay — đều dùng PKCE. Điều này quan trọng nhất với flow dán tay, nơi kết quả đăng nhập đi qua clipboard của bạn trên đường quay lại SkimMail. Hai khoá độc lập bảo vệ nó: client secret của bạn — provider đòi hỏi trước khi đổi code lấy token, và secret này không bao giờ rời khỏi server của bạn — và PKCE, một giá trị dùng một lần server của bạn tự giữ và provider kiểm tra lại. Chỉ riêng secret đã khiến một dòng bị copy trở nên vô dụng với bất kỳ ai khác nhìn thấy nó; PKCE giữ điều đó đúng ngay cả khi một provider từng xử lý PKCE ở phía họ chưa tốt. Không khoá nào cần phải hoàn hảo một mình. Quy trình đầy đủ: Accounts.

Ticket nội bộ mang theo một lượt đăng nhập giờ nói rõ nó dùng để làm gì. Hai ticket ngắn hạn không liên quan — một cái giữ trong lúc đăng nhập Google hoặc Microsoft, và một cái giữ giữa mật khẩu của bạn và mã 2FA — từng được niêm phong theo cùng một cách, với tên field chồng lấn nhau, nên về nguyên tắc cái này có thể bị đọc nhầm thành cái kia. Chưa từng bị khai thác theo cách này; giờ điều đó không thể xảy ra vì cách xây dựng, chứ không phải nhờ may mắn.

Encryption at rest

SkimMail mã hoá dữ liệu at-rest theo hai lớp độc lập, và chúng che chắn những thứ khác nhau. Biết một setting thuộc lớp nào cho bạn biết chính xác nó bảo vệ gì và không bảo vệ gì — và 1.11.0 đổi đúng một điều: nội dung thư đã cache giờ được mã hoá at-rest, còn mọi thứ khác về thư của bạn thì không.

Lớp Che chắn Cách làm
1 — Bọc secret Credential tài khoản, OAuth token, TOTP secret, secret của proxy/tunnel, secret key của S3 dùng cho backup Luôn AES-256-GCM, dưới một master key 32 byte. KEY_PROVIDER quyết định chính key đó được lưu ra sao.
2 — Nội dung Nội dung thư đã cache (DATA_DIR/blobs/) AES-256-GCM từ 1.11.0, dưới một key dẫn xuất từ master key. Luôn bật trừ khi BLOB_ENCRYPT được đặt tường minh thành false, 0, no hoặc off.
2 — Nội dung Subject thư, người gửi/nhận, file skimmail.db, chỉ mục tìm kiếm full-text SkimMail không mã hoá. Hãy mã hoá volume bên dưới thay vào đó.

Lớp 1 — KEY_PROVIDER

  • file (mặc định) — master key là 32 byte thô tại DATA_DIR/master.key, quyền 0600. Bảo vệ chỉ dựa vào quyền hệ thống file: ai copy được DATA_DIR là có cả key lẫn ciphertext cạnh nhau, giải mã được mọi credential đã lưu. Chỉ an toàn khi bản thân volume đã được mã hoá (Lớp 2).
  • passphrase (khuyến nghị) — master key được sinh một lần rồi lưu ở dạng đã bọc trong DATA_DIR/keyring.json, mã hoá bằng một key-encryption-key dẫn xuất (Argon2id) từ một unlock secret riêng, nằm ngoài data directory. Một DATA_DIR bị copy khi đó vô dụng nếu thiếu secret này. Nó cố tình không phải mật khẩu đăng nhập của bạn — sync vẫn chạy không cần người trông coi qua các lần restart, nên cần một secret mà tiến trình nền tự đọc được. Đổi secret bất cứ lúc nào bằng skimmail rekey, lệnh này chỉ bọc lại cùng một key mà không mã hoá lại bất kỳ dữ liệu nào.

Kết hợp passphrase với một volume đã mã hoá nghĩa là một ổ đĩa hay backup bị lấy trộm không lộ cả credential lẫn thư của bạn. file trên một volume chưa mã hoá — mặc định zero-config — lộ cả hai.

Lớp 2 — nội dung thư

Nội dung thư đã cache được mã hoá từ 1.11.0 — AES-256-GCM, dưới một key dẫn xuất từ master key. Không phát sinh bí mật mới nào phải backup: key được dẫn xuất chứ không lưu lại. Nội dung do các bản trước ghi vẫn đọc được, nên nâng cấp không cần migration, và tắt setting này đi cũng không bao giờ làm mất cache đang có. Hành vi đầy đủ: Cache nội dung thư.

BLOB_ENCRYPT luôn bật trừ khi bạn tắt nó đi. Chỉ false, 0, nooff mới tắt; mọi giá trị khác, kể cả 1TRUE, đều để nó bật.

1.11.0 làm ngược điều này. Bản đó chỉ chấp nhận đúng chữ true, nên BLOB_ENCRYPT=1 — hay yes, on, TRUE — lặng lẽ tắt mã hoá. Đã sửa ở 1.11.1. Nếu bạn từng đặt một trong các giá trị đó trên 1.11.0, nội dung thư cache từ lúc ấy không được mã hoá và cũng không được ghi đè tại chỗ; hãy đẩy chúng ra bằng cách hạ giới hạn dung lượng hoặc tuổi trong Settings ▸ Security ▸ Cache nội dung thư, rồi chúng sẽ quay lại ở dạng đã niêm phong.

Mọi thứ còn lại về thư của bạn vẫn là plaintext trên đĩa, cố tình như vậy: subject, địa chỉ, ngày, trích đoạn và chỉ mục tìm kiếm full-text nằm trong database, mà mã hoá chúng trong app sẽ phá vỡ bản build pure-Go và phá luôn chính việc tìm kiếm. Biện pháp giảm thiểu được ghi nhận cho những thứ đó là mã hoá volume, không phải mã hoá nội dung — LUKS/dm-crypt hoặc một ZFS dataset đã mã hoá bên dưới DATA_DIR, ổ đĩa cloud đã mã hoá (EBS, disk encryption của GCP/Azure) bên dưới volume của container, hoặc S3 server-side encryption (SSE-S3/SSE-KMS) nếu blob backend của bạn là S3. Bất kỳ cách nào trong số này che chắn cùng lúc file database, nội dung thư đã cache, và file chứa key thô, ở tầng lưu trữ chứ không phải bên trong app.

Một DATA_DIR bị lấy trộm sẽ lộ ra gì:

Cấu hình Credential / token Nội dung thư đã cache Subject, địa chỉ, chỉ mục tìm kiếm
file + volume chưa mã hoá (mặc định) lộ lộ* lộ
passphrase + volume chưa mã hoá được bảo vệ được bảo vệ* vẫn lộ
file + volume đã mã hoá được bảo vệ (nhờ volume) được bảo vệ được bảo vệ (nhờ volume)
passphrase + volume đã mã hoá được bảo vệ hai lớp được bảo vệ hai lớp được bảo vệ (nhờ volume)

* Mã hoá nội dung thư chỉ chắc chắn bằng đúng master key mà nó dẫn xuất ra. Với KEY_PROVIDER=file, key đó nằm ngay trong cùng thư mục, nên ai copy được DATA_DIR là giải mã được cache — nó chỉ nâng rào so với một lệnh grep tuỳ hứng, không hơn. Với passphrase, key không nằm trên đĩa, và nội dung thư đã cache thật sự không đọc được nếu thiếu unlock secret.

Những gì trang này không che chắn

  • Phân quyền theo từng user có từ 1.10.0, và tới 1.13.0 mới phủ hết mọi bề mặt. Mỗi account, group, thư, attachment và cờ người gửi thuộc về đúng một user, và điều đó được thực thi ở tầng dữ liệu chứ không phải ở giao diện; yêu cầu vượt ranh giới nhận lại "không tìm thấy" chứ không phải "không có quyền" — vì từ chối một dòng cụ thể chính là xác nhận dòng đó tồn tại. Vai trò gồm owner, operatorviewer; ranh giới giữa chúng là một bước phân quyền duy nhất trong middleware, nên không thể thêm route mà quên quyết định ai được gọi. Đó là mô tả thiết kế từ 1.10.0 trở đi; nó chưa mô tả đúng mọi câu truy vấn cho tới 1.13.0 — riêng cờ người gửi thì suốt thời gian đó vẫn áp cho toàn instance. Trên mọi bản TRƯỚC 1.10.0, một "user" thứ hai là một admin thứ hai chứ không phải chủ sở hữu một hộp thư riêng. Chi tiết đầy đủ, gồm bảng vai trò và lệnh skimmail user: Người dùng và phân quyền.

  • Một link trong thư không còn thao túng được instance của chính bạn nữa. Việc làm cho frame của lá thư same-origin — thay đổi mà 1.18.0 cần để ảnh từ xa tải được cùng session của bạn, xem Ảnh từ xa trong thư — cũng có nghĩa một link trông bình thường trong thư của người lạ có thể trỏ ngược về chính địa chỉ của SkimMail và được theo (follow) trong khi bạn đang đăng nhập, biến một cú click thành một request thực hiện nhân danh bạn. Từ 1.18.0 (SKIMMAIL-209), một link trong thư trỏ tới bất kỳ đâu khác ngoài ra thế giới bên ngoài đều vô hại (inert). Một anchor trỏ tới một vị trí trong cùng thư vẫn hoạt động, và mailto: cũng vậy.

  • Mở một lá thư vẫn có thể báo cho người gửi biết. Nếu bạn bấm Show images, SkimMail đi lấy ảnh từ server của người gửi — qua egress của account đó, không cookie và không Referer, nhưng bản thân lượt lấy đó nói với người gửi rằng thư đã được mở và mở lúc nào. Ảnh mặc định bị giữ lại, một người gửi có thể bị từ chối vĩnh viễn, và cả tính năng có thể tắt cho toàn instance từ Settings ▸ Security ▸ Ảnh từ xa (chỉ owner, có màn hình từ 1.11.1). Thứ nó không làm được là khiến một tấm ảnh đã lấy trở nên vô hình. Xem Ảnh từ xa trong thư.

  • Hãy nâng lên 1.14.0 nếu có nhiều hơn một người đăng nhập. Từ 1.10.0 đến 1.13.0 có mười ba thao tác được chặn theo vai trò nhưng không chặn theo chủ sở hữu, nên chúng đọc hoặc ghi vượt qua đúng cái ranh giới user mà chúng phải bảo vệ. 1.12.0 bịt năm cái: Reset groups xoá group của mọi user trên toàn instance, snooze và pin có thể áp lên thư của người khác, một device nhận push có thể bị xoá khỏi tài khoản người khác, và số đếm split-inbox được tính trên thư của tất cả mọi người. 1.13.0 bịt thêm sáu cái nữa, trong đó hai cái một viewer bình thường chạm tới được qua đúng màn hình mà sản phẩm có sẵn:

    Nó đã làm sai điều gì
    Tìm kiếm trả về kết quả từ mọi tài khoản trên instance — tiêu đề, người gửi, dòng preview và cả bản tóm tắt AI đã cache
    Digest AI "catch me up" đọc những dòng đầu của nội dung thư chưa đọc của mọi người rồi gửi sang nhà cung cấp AI của instance
    Archive / Move / Delete chuyển thư của người khác trên chính mail server của họ trước khi kiểm tra quyền sở hữu, rồi báo về là đã thành công
    Tóm tắt AI đọc được xuyên tài khoản qua cache kết quả, vì cache trả lời trước khi kiểm tra quyền sở hữu
    Tài khoản thêm bằng OAuth được lưu mà không có chủ: thuộc về không ai cả, biến mất khỏi danh sách tài khoản, và hai người kết nối cùng một hộp thư thì đè lên nhau
    Mute người gửi áp cho toàn instance, còn những mute/VIP bạn đặt lại được ghi vào nơi không ai đọc lại

    Digest chỉ chạy khi instance đã bật AI và bật tính năng digest. Nhưng lưu ý điều này không giống với "tab AI đang bị ẩn ở bản này": ẩn một tab là quyết định về những gì giao diện vẽ ra, không phải một ranh giới bảo mật. Route vẫn được đăng ký và vẫn trả lời. Hãy coi GET /api/digest là đang sống.

    1.14.0 bịt nốt hai cái cuối, và chúng khác hình dạng: chúng không đọc dòng dữ liệu của người khác, chúng hành động thay người khác. Tìm kiếm phía máy chủ và nút Test connection của tài khoản đều lấy id tài khoản thẳng từ request rồi mở kết nối IMAP của tài khoản đó bằng mật khẩu đã lưu và đường mạng của nó — tìm trên hộp thư thật và ghi các header lấy về vào cơ sở dữ liệu. Không có gì quay về tay người gọi; nhưng kết nối vẫn mở và các dòng vẫn được ghi, và một viewer cũng làm được. Cả hai đã bị cổng kiểm tra thường trực gắn cờ rồi bị gạt đi bởi một dòng miễn trừ không đúng sự thật.

    Instance một người dùng — tức mặc định — chưa bao giờ bị ảnh hưởng, vì không có user thứ hai nào để vươn sang. Chi tiết, và việc nâng cấp làm gì với danh sách người gửi bị mute của bạn: Người dùng và phân quyền.

    1.18.0 bịt thêm một lỗ nữa, được phát hiện qua một đợt audit nội bộ chứ không phải từ báo cáo. SyncAccount — hàm đứng sau nút Sync now — chưa bao giờ kiểm tra ai là chủ của account id nó nhận được, dù caller của nó, POST /api/accounts/{id}/sync, là một route mức operator với một caller xuyên-user có thật: nút sync ở sidebar, bấm trên bất kỳ account nào operator nhìn thấy. Gõ id account của người khác khiến server dial hộp thư của họ bằng credential đã giải mã của họ qua egress của họ, báo về số thư tìm thấy, hiện nguyên văn lỗi từ mail server khi thất bại, và — với một account Gmail hoặc Outlook — ép một lượt refresh token xoay vòng lượt đăng nhập của họ. Cùng một lệnh gọi đó còn có thể đẩy bộ đếm lỗi của người khác lên tới khi cơ chế tự động cắt của SkimMail tắt hộp thư của họ, kèm một thông báo lỗi do người gọi tự chọn để lại trên màn hình Accounts của người đó. Đây là hàm thứ ba có đúng hình dạng này: TestAccountConnectionWatchAccount đều đã được cấp cùng một kiểm tra quyền sở hữu từ 1.14.0, còn SyncAccount bị bỏ sót — và nó là hàm duy nhất trong ba hàm có một caller xuyên-user thật đang sống. Nó có mặt trong mọi tag đã phát hành từ 1.10.0 đến 1.17.0, mười một tag, trọn vẹn khoảng thời gian kể từ khi khái niệm quyền sở hữu theo-từng-user tồn tại. Một instance một người dùng chưa bao giờ bị ảnh hưởng, vì không có account thứ hai nào để gõ vào. Nếu bạn chạy SkimMail cho nhiều hơn một người, hãy đọc mục Upgrading trong chính mục ## 1.18.0 của CHANGELOG.md để biết cần kiểm tra gì trên instance của bạn — nó được viết cho việc đó, trang này sẽ không nhắc lại.

  • Các endpoint AI không có kiểm tra licence hay tier nào, và việc ẩn nút bấm không phải là một ranh giới bảo mật. Sáu route — tóm tắt, phân loại, dịch, trích action item, tóm tắt thread, và digest hộp thư — được đăng ký trong mọi bản phát hành và bất kỳ ai giữ session nào cũng gọi được trực tiếp, bất kể giao diện đang hiện gì bên cạnh chúng. Mỗi route gửi nội dung thư liên quan tới bất kỳ AI provider nào bạn đã cấu hình; chưa cấu hình cái nào thì mọi route đều lỗi và không có gì rời khỏi server của bạn. Điều này từng bị mô tả sai từ 1.10.0 đến 1.17.0, vốn nói tính năng này cần licence Pro và không hề tồn tại ở Community hay Sponsor. Xem mục "AI features" của PRIVACY.md để biết bản đính chính đầy đủ, và coi mọi route trong số này đang sống, bất kể tab nào đang hiển thị.

  • Một host đã bị root và đang chạy nằm ngoài phạm vi của mọi cơ chế ở trên. Claim code, screen lock, và cả hai lớp encryption đều giả định kẻ tấn công chưa có sẵn mức truy cập đó vào tiến trình đang sống.


SkimMail · skimmail@base101.app · 2026-09-18 · commit f525934

Clone this wiki locally