fix(avatars): every signed-in account sees other users' pictures

System Health shows every user's name and avatarAt to everyone, but the
picture route GET /api/users/:id/avatar sat under the staff-only /users
prefix, so a developer got 403 EADMIN and saw only initials. authGuard now
lets any signed-in account make that one read (EVERYONE_READS); every
other /users path stays staff-only, and the test checks both.
This commit is contained in:
2026-11-02 16:40:29 +07:00
parent 3ec1978176
commit 600e8eec6a
6 changed files with 22 additions and 11 deletions
+9 -4
View File
@@ -4,8 +4,9 @@
* server/lib/avatars.js): an account sets, reads and removes its own picture;
* only PNG/JPEG/GIF/WebP by magic bytes (an SVG is refused), at most 1 MB,
* served with `nosniff` and a `sandbox` CSP; `avatarAt` reaches `/api/auth/me`;
* another account never sees it through `/api/me`; staff read it at
* `/api/users/:id/avatar` (a developer is refused) and a workspace carries
* another account never sees it through `/api/me`; every account reads it at
* `/api/users/:id/avatar` (a developer too — only the picture, the rest of
* `/users` stays staff-only) and a workspace carries
* `owner_avatar_at` for the sidebar's owner groups. A picture from a link
* (`POST /api/me/avatar/url`, server/lib/avatar-url.js) is stored, never
* downloaded, and the avatar routes redirect to it; https only, never a
@@ -196,7 +197,7 @@ it("a picture from a link: https only, never a private or local address", async
}
});
it("staff read another account's avatar; workspaces carry owner_avatar_at", async () => {
it("every account reads another's avatar; workspaces carry owner_avatar_at", async () => {
const ava = await login("ava", "ava-pass-1");
const ann = await login("ann", "ann-pass-1");
const bob = await login("bob", "bob-pass-1");
@@ -210,8 +211,12 @@ it("staff read another account's avatar; workspaces carry owner_avatar_at", asyn
assert.deepEqual(Buffer.from(await got.arrayBuffer()), PNG);
assert.equal(
(await fetch(`${BASE}/api/users/${id}/avatar`, { headers: { cookie: bob } })).status,
403
200
);
// Only the picture: the rest of /users stays staff-only for a developer.
for (const path of [`/api/users/${id}`, `/api/users/${id}/details`, "/api/users"]) {
assert.equal((await fetch(BASE + path, { headers: { cookie: bob } })).status, 403, path);
}
const lanes = (await (await fetch(BASE + "/api/lanes", { headers: { cookie: ann } })).json())
.lanes;
assert.ok(lanes.find((l) => l.cwd === "/w/ava").owner_avatar_at);