mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-26 01:01:40 +00:00
fix(security): harden dashboard API against unauthenticated access (#9800)
Addresses responsible disclosure from FuzzMind Security Lab (CVE pending). The web dashboard API server had 36 endpoints, of which only 5 checked the session token. The token itself was served from an unauthenticated GET /api/auth/session-token endpoint, rendering the protection circular. When bound to 0.0.0.0 (--host flag), all API keys, config, and cron management were accessible to any machine on the network. Changes: - Add auth middleware requiring session token on ALL /api/ routes except a small public whitelist (status, config/defaults, config/schema, model/info) - Remove GET /api/auth/session-token endpoint entirely; inject the token into index.html via a <script> tag at serve time instead - Replace all inline token comparisons (!=) with hmac.compare_digest() to prevent timing side-channel attacks - Block non-localhost binding by default; require --insecure flag to override (with warning log) - Update frontend fetchJSON() to send Authorization header on all requests using the injected window.__HERMES_SESSION_TOKEN__ Credit: Callum (@0xca1x) and @migraine-sudo at FuzzMind Security Lab
This commit is contained in:
parent
b583210c97
commit
99bcc2de5b
4 changed files with 152 additions and 56 deletions
|
|
@ -1,11 +1,22 @@
|
|||
const BASE = "";
|
||||
|
||||
// Ephemeral session token for protected endpoints (reveal).
|
||||
// Fetched once on first reveal request and cached in memory.
|
||||
// Ephemeral session token for protected endpoints.
|
||||
// Injected into index.html by the server — never fetched via API.
|
||||
declare global {
|
||||
interface Window {
|
||||
__HERMES_SESSION_TOKEN__?: string;
|
||||
}
|
||||
}
|
||||
let _sessionToken: string | null = null;
|
||||
|
||||
async function fetchJSON<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${BASE}${url}`, init);
|
||||
// Inject the session token into all /api/ requests.
|
||||
const headers = new Headers(init?.headers);
|
||||
const token = window.__HERMES_SESSION_TOKEN__;
|
||||
if (token && !headers.has("Authorization")) {
|
||||
headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
const res = await fetch(`${BASE}${url}`, { ...init, headers });
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => res.statusText);
|
||||
throw new Error(`${res.status}: ${text}`);
|
||||
|
|
@ -15,9 +26,12 @@ async function fetchJSON<T>(url: string, init?: RequestInit): Promise<T> {
|
|||
|
||||
async function getSessionToken(): Promise<string> {
|
||||
if (_sessionToken) return _sessionToken;
|
||||
const resp = await fetchJSON<{ token: string }>("/api/auth/session-token");
|
||||
_sessionToken = resp.token;
|
||||
return _sessionToken;
|
||||
const injected = window.__HERMES_SESSION_TOKEN__;
|
||||
if (injected) {
|
||||
_sessionToken = injected;
|
||||
return _sessionToken;
|
||||
}
|
||||
throw new Error("Session token not available — page must be served by the Hermes dashboard server");
|
||||
}
|
||||
|
||||
export const api = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue