mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-21 22:52:19 +03:00
fix(security): mask the Obsidian WebDAV password from non-management callers
GET /api/settings/obsidian/webdav returned the plaintext webdavPassword to any caller the handler admitted — including an anonymous caller reaching it through the requireLogin=false open mode (the default management pipeline already blocks non-manage keys). The plaintext is now returned only to a genuine management principal (dashboard session or manage-scope key); everyone else gets a `webdavPasswordSet` flag instead. The dashboard's authenticated reveal view is unchanged. Reported by @0raN9ewww via GHSA-62vw-4m6w-cqqq (and the credential-exposure portion of GHSA-p855-p6fm-76r3).
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { buildErrorBody } from "@omniroute/open-sse/utils/error";
|
||||
import {
|
||||
getObsidianSyncStatus,
|
||||
@@ -21,10 +22,19 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
try {
|
||||
const status = await getObsidianSyncStatus();
|
||||
// GHSA-62vw: the WebDAV password is reusable authentication material. Return
|
||||
// the plaintext only to a genuine management principal (dashboard session or
|
||||
// manage-scope key), never to an anonymous caller that reached this handler
|
||||
// through the requireLogin=false open mode. The dashboard's authenticated
|
||||
// reveal-password view is unaffected; anonymous callers get a set/unset flag.
|
||||
const hasManagement =
|
||||
(await requireManagementAuth(request, { alwaysRequireAuth: true })) === null;
|
||||
return NextResponse.json({
|
||||
webdavEnabled: status.webdavEnabled,
|
||||
webdavUsername: status.webdavEnabled ? status.webdavUsername : null,
|
||||
webdavPassword: status.webdavEnabled ? status.webdavPassword : null,
|
||||
webdavPassword:
|
||||
status.webdavEnabled && hasManagement ? status.webdavPassword : null,
|
||||
webdavPasswordSet: status.webdavEnabled && Boolean(status.webdavPassword),
|
||||
vaultPath: status.vaultPath,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -114,7 +114,46 @@ test("POST with a valid temp dir → returns { username, password }, GET shows e
|
||||
const getBody = (await getRes.json()) as Record<string, unknown>;
|
||||
assert.equal(getBody.webdavEnabled, true);
|
||||
assert.ok(typeof getBody.webdavUsername === "string" && (getBody.webdavUsername as string).length > 0);
|
||||
assert.ok(typeof getBody.webdavPassword === "string" && (getBody.webdavPassword as string).length > 0);
|
||||
// Anonymous GET (this request carries no management credential): the plaintext
|
||||
// password is masked (GHSA-62vw), but the set/unset flag still reflects state.
|
||||
assert.equal(getBody.webdavPassword, null);
|
||||
assert.equal(getBody.webdavPasswordSet, true);
|
||||
} finally {
|
||||
fs.rmSync(vaultDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("GET masks the WebDAV password for anonymous callers but reveals it to a management session (GHSA-62vw)", async () => {
|
||||
const vaultDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-vault-62vw-"));
|
||||
try {
|
||||
// Enable WebDAV so there is a stored password to leak.
|
||||
const enableRes = await route.POST(
|
||||
makeRequest("http://localhost/api/settings/obsidian/webdav", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ vaultPath: vaultDir }),
|
||||
})
|
||||
);
|
||||
assert.equal(enableRes.status, 200);
|
||||
|
||||
// Anonymous (open-mode) caller: password masked, flag still set.
|
||||
const anonBody = (await (await route.GET(
|
||||
makeRequest("http://localhost/api/settings/obsidian/webdav")
|
||||
)).json()) as Record<string, unknown>;
|
||||
assert.equal(anonBody.webdavEnabled, true);
|
||||
assert.equal(anonBody.webdavPassword, null, "anonymous caller must not receive the plaintext password");
|
||||
assert.equal(anonBody.webdavPasswordSet, true);
|
||||
|
||||
// Genuine management session: the operator's reveal-password view still works.
|
||||
const sessionReq = (await makeManagementSessionRequest(
|
||||
"http://localhost/api/settings/obsidian/webdav"
|
||||
)) as unknown as NextRequest;
|
||||
const sessionBody = (await (await route.GET(sessionReq)).json()) as Record<string, unknown>;
|
||||
assert.ok(
|
||||
typeof sessionBody.webdavPassword === "string" &&
|
||||
(sessionBody.webdavPassword as string).length > 0,
|
||||
"a management session must still receive the plaintext password"
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(vaultDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user