mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(web_server,whatsapp-bridge): validate Host header against bound interface (#13530)
DNS rebinding attack: a victim browser that has the dashboard (or the WhatsApp bridge) open could be tricked into fetching from an attacker-controlled hostname that TTL-flips to 127.0.0.1. Same-origin and CORS checks don't help — the browser now treats the attacker origin as same-origin with the local service. Validating the Host header at the app layer rejects any request whose Host isn't one we bound for. Changes: hermes_cli/web_server.py: - New host_header_middleware runs before auth_middleware. Reads app.state.bound_host (set by start_server) and rejects requests whose Host header doesn't match the bound interface with HTTP 400. - Loopback binds accept localhost / 127.0.0.1 / ::1. Non-loopback binds require exact match. 0.0.0.0 binds skip the check (explicit --insecure opt-in; no app-layer defence possible). - IPv6 bracket notation parsed correctly: [::1] and [::1]:9119 both accepted. scripts/whatsapp-bridge/bridge.js: - Express middleware rejects non-loopback Host headers. Bridge already binds 127.0.0.1-only, this adds the complementary app-layer check for DNS rebinding defence. Tests: 8 new in tests/hermes_cli/test_web_server_host_header.py covering loopback/non-loopback/zero-zero binds, IPv6 brackets, case insensitivity, and end-to-end middleware rejection via TestClient. Reported in GHSA-ppp5-vxwm-4cf7 by @bupt-Yy-young. Hardening — not CVE per SECURITY.md §3. The dashboard's main trust boundary is the loopback bind + session token; DNS rebinding defeats the bind assumption but not the token (since the rebinding browser still sees a first-party fetch to 127.0.0.1 with the token-gated API). Host-header validation adds the missing belt-and-braces layer.
This commit is contained in:
parent
16accd44bd
commit
244ae6db15
3 changed files with 268 additions and 0 deletions
|
|
@ -372,6 +372,37 @@ async function startSocket() {
|
|||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Host-header validation — defends against DNS rebinding.
|
||||
// The bridge binds loopback-only (127.0.0.1) but a victim browser on
|
||||
// the same machine could be tricked into fetching from an attacker
|
||||
// hostname that TTL-flips to 127.0.0.1. Reject any request whose Host
|
||||
// header doesn't resolve to a loopback alias.
|
||||
// See GHSA-ppp5-vxwm-4cf7.
|
||||
const _ACCEPTED_HOST_VALUES = new Set([
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
'[::1]',
|
||||
'::1',
|
||||
]);
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const raw = (req.headers.host || '').trim();
|
||||
if (!raw) {
|
||||
return res.status(400).json({ error: 'Missing Host header' });
|
||||
}
|
||||
// Strip port suffix: "localhost:3000" → "localhost"
|
||||
const hostOnly = (raw.includes(':')
|
||||
? raw.substring(0, raw.lastIndexOf(':'))
|
||||
: raw
|
||||
).replace(/^\[|\]$/g, '').toLowerCase();
|
||||
if (!_ACCEPTED_HOST_VALUES.has(hostOnly)) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid Host header. Bridge accepts loopback hosts only.',
|
||||
});
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Poll for new messages (long-poll style)
|
||||
app.get('/messages', (req, res) => {
|
||||
const msgs = messageQueue.splice(0, messageQueue.length);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue