fix(security): unauth share_token leak (HIGH) + restore path-traversal, logo file-read, branding path keys (#946)

* fix(security): stop unauth share_token leak + block restore path-traversal, logo-path file read, branding path keys

* test: update resolveLogoFile for the c7x5 containment (reject outside-storage absolute paths, keep inside)

* fix(security): codex round-1 — escape LIKE wildcards in share-link resolve, keep in-storage absolute logos, guard restore verification

- shareLinkService: escape %/_ in the link_partial LIKE fallback so an
  anonymous /resolve/____… wildcard can't match an arbitrary share_link and
  leak its bearer token (reopened GHSA-rh8r). Explicit ESCAPE for SQLite.
- resolveLogoFile: re-add the raw absolute candidate but keep it subject to
  the storage-root containment filter (GHSA-c7x5) so legit in-storage
  absolute logos resolve while /etc/passwd stays rejected.
- restoreService: apply the same pathEscapes guard in post-restore
  verification so a skipped traversal entry isn't fs.access'd/hashed.

---------

Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
Paul Nothaft
2026-08-02 08:37:48 +02:00
committed by GitHub
co-authored by Paul Nothaft
parent 8cbb37310b
commit 9050affd8d
7 changed files with 187 additions and 12 deletions
+10 -1
View File
@@ -159,7 +159,16 @@ const resolveShareIdentifier = async (identifier) => {
return { event, matchType: 'link', shareToken: getEventShareToken(event) };
}
event = await baseQuery.clone().where('share_link', 'like', `%/${trimmed}`).first();
// GHSA-rh8r hardening: `trimmed` is attacker-controlled, so escape LIKE
// wildcards (`%`, `_`, and the escape char itself) before embedding it.
// Otherwise an anonymous `/resolve/________…________` (32 underscores)
// matches ANY share_link via single-char wildcards, resolves as
// matchType 'link_partial', and the /resolve route hands back the
// gallery's bearer token — reopening the very hole the token-withholding
// fix closed. Explicit ESCAPE clause because SQLite has no default LIKE
// escape character (Postgres defaults to backslash, but we set it for both).
const likeTail = `%/${trimmed.replace(/[\\%_]/g, (c) => `\\${c}`)}`;
event = await baseQuery.clone().whereRaw('share_link LIKE ? ESCAPE \'\\\'', [likeTail]).first();
if (event) {
return { event, matchType: 'link_partial', shareToken: getEventShareToken(event) };
}