fix(proxy): harden security and add UpSnap debug logging

- XSS: escape serviceName in waking page HTML
- Session TTL: 24h expiration with periodic cleanup
- Rate limit: 5 login attempts / 15 min per IP
- CORS: restrict to same-origin + localhost
- SSRF: block localhost/metadata in service targets
- UpSnap: log response bodies on auth/wake failures

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Vadim Sobinin
2026-03-15 00:57:38 +03:00
parent f38c944690
commit 719afa8533
8 changed files with 143 additions and 34 deletions

View File

@@ -8,23 +8,24 @@ async function authenticate(): Promise<string> {
const collections = ['_superusers', 'users'];
for (const collection of collections) {
const res = await fetch(
`${config.upsnap.url}/api/collections/${collection}/auth-with-password`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
identity: config.upsnap.username,
password: config.upsnap.password,
}),
}
);
const url = `${config.upsnap.url}/api/collections/${collection}/auth-with-password`;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
identity: config.upsnap.username,
password: config.upsnap.password,
}),
});
if (res.ok) {
const data = (await res.json()) as UpSnapAuthResponse;
token = data.token;
return token;
}
const errBody = await res.text().catch(() => '');
console.error(`[UpSnap] Auth attempt ${collection} failed (${res.status}): ${errBody}`);
}
throw new Error('UpSnap auth failed: could not authenticate as superuser or user');
@@ -67,15 +68,18 @@ export async function wakeDevice(): Promise<void> {
{ method: 'PATCH', body: JSON.stringify({ status: 'on' }) }
);
// UpSnap may also have a dedicated wake endpoint — try both approaches
if (!res.ok) {
// Fallback: POST to wake endpoint
const res2 = await upSnapFetch(`/api/upsnap/wake/${config.upsnap.deviceId}`, {
method: 'GET',
});
if (!res2.ok) {
throw new Error(`UpSnap wake failed: ${res2.status}`);
}
if (res.ok) return;
const body1 = await res.text().catch(() => '');
console.error(`[UpSnap] PATCH wake failed (${res.status}): ${body1}`);
// Fallback: GET wake endpoint
const res2 = await upSnapFetch(`/api/upsnap/wake/${config.upsnap.deviceId}`, {
method: 'GET',
});
if (!res2.ok) {
const body2 = await res2.text().catch(() => '');
throw new Error(`UpSnap wake failed: ${res2.status}${body2}`);
}
}