fix(sqlite adapter): prevent path traversal via user-controlled dbPath#371
fix(sqlite adapter): prevent path traversal via user-controlled dbPath#371sebastiondev wants to merge 1 commit into
Conversation
| ? userDbPath.trim() | ||
| : DEFAULT_DB_RELATIVE; | ||
|
|
||
| if (path.isAbsolute(relative)) { |
There was a problem hiding this comment.
We had one person in the past who created exactly the same pr and we rejected it, because this change (line 25) can and will break existing installations using this adapter if they have an absolute path.
What's your take on this
|
Thanks for the context — that's a fair objection, and you're right that a hard rejection of absolute paths is a breaking change for anyone who legitimately configured e.g. The reason I still think there's something worth fixing here is the trust boundary rather than the path shape itself: Given your constraint, some options that preserve existing installs:
My preference would be (3) — smallest behavioural change, doesn't touch the adapter at all, and matches the actual problem (authorization, not path validation). Happy to rework the PR that way, or (1) if you'd rather keep the check next to the adapter. Or if you'd prefer to leave this as-is and treat "any authenticated user can configure adapters" as intended, I'm fine closing the PR — your call. |
Summary
The SQLite notification adapter (
lib/notification/adapter/sqlite.js) uses the user-controlleddbPathfield verbatim to create directories and open a database file. Because notification adapter fields are stored per-job by any authenticated user viaPOST /api/jobs, a non-admin user can supply an arbitrary absolute path or a../-containing relative path and cause Fredy to:fs.mkdirSync(dbDir, { recursive: true })), andnew Database(dbPath)).This is CWE-22 (Path Traversal / arbitrary file & directory creation). Severity is moderate: it requires an authenticated account, but Fredy explicitly supports multiple non-admin users, and this capability goes well beyond what the UI is meant to expose (writing outside the app directory, potentially clobbering unrelated files, or seeding files into sensitive locations like
~/.ssh/, cron directories, or a reverse-proxy webroot depending on how the container/host is provisioned).Data flow
POST /api/jobsaccepts a job payload includingnotificationAdapter[].fields.dbPathfrom any authenticated session (seeauthHookinlib/api).send()inlib/notification/adapter/sqlite.jsreadssqliteConfig?.fields?.dbPathand passes it directly topath.dirname,fs.mkdirSync, andnew Database().No sanitisation, base-directory containment, or absolute-path rejection existed on this path prior to the fix.
Fix
Introduce
resolveSafeDbPath()which:process.cwd()(the Fredy application directory), defaulting todb/listings.dbwhen unset/blank,path.resolveand verifies it is either equal to the base directory or starts withbaseDir + path.sep(the trailing-separator check is important — a naivestartsWith(baseDir)would allow/app-evilto bypass a/appprefix check),The adapter's field description is also updated so the UI communicates the new constraint.
The change is contained to a single file (28 insertions, 2 deletions) and preserves backward compatibility for the documented, intended usage (
db/listings.dbor other relative paths under the app directory).Proof of concept
As any authenticated non-admin user:
Relative traversal (
"dbPath":"../../etc/cron.d/evil.db") is likewise blocked becausepath.resolve(process.cwd(), '../../etc/cron.d/evil.db')lands outsideprocess.cwd()and fails the containment check.Testing
lib/notification/adapter/sqlite.jsbefore the change:sqliteConfig?.fields?.dbPathflowed unchanged intofs.mkdirSyncandnew Database.resolveSafeDbPath()with:<cwd>/db/listings.db(unchanged default behaviour),"db/listings.db"→ resolves inside cwd (accepted),"/etc/passwd"→ rejected (absolute path),"../../etc/cron.d/x"→ rejected (escapes cwd),"db/../db/listings.db"→ accepted (normalises inside cwd).git diff master..HEAD --statshows the change is isolated to one file.Adversarial review
Before submitting we tried to talk ourselves out of this. The endpoint is authenticated, so is it really a boundary crossing? Yes: Fredy documents and implements a distinction between admin and non-admin users, and neither role is intended to have arbitrary filesystem write on the host. We also considered whether the notification adapter is only reachable to admins — it isn't; the job/adapter APIs are gated by the generic session
authHook, not an admin check. Finally, we considered whetherbetter-sqlite3itself normalises or restricts the path — it doesn't; it opens whatever path you give it and will happily create the file. The containment check is the right layer for this fix.Discovered by the Sebastion AI GitHub App.