SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
This commit is contained in:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,136 @@
|
||||
import { Hono } from 'hono';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
clearAvatarSessionCookie,
|
||||
extractWebuiSessionToken,
|
||||
LOGIN_BODY_LIMIT_BYTES,
|
||||
readAvatarSessionToken,
|
||||
registerLoginRequestSecurity,
|
||||
setAvatarSessionCookie,
|
||||
webuiSecurityHeaders,
|
||||
} from '../src/webui/request-security';
|
||||
|
||||
describe('WebUI public request security', () => {
|
||||
it('rejects an oversized login body before JSON parsing', async () => {
|
||||
const app = new Hono();
|
||||
app.use('*', webuiSecurityHeaders);
|
||||
registerLoginRequestSecurity(app);
|
||||
app.post('/api/login', async (c) => c.json(await c.req.json()));
|
||||
|
||||
const response = await app.request('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ password: 'x'.repeat(LOGIN_BODY_LIMIT_BYTES) }),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(413);
|
||||
expect(response.headers.get('x-frame-options')).toBe('DENY');
|
||||
await expect(response.json()).resolves.toMatchObject({ success: false });
|
||||
});
|
||||
|
||||
it('accepts normal JSON login requests and rejects misleading content types', async () => {
|
||||
const app = new Hono();
|
||||
registerLoginRequestSecurity(app);
|
||||
app.post('/api/login', async (c) => c.json(await c.req.json()));
|
||||
|
||||
const accepted = await app.request('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/problem+json' },
|
||||
body: JSON.stringify({ password: 'normal-password' }),
|
||||
});
|
||||
const rejected = await app.request('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'text/plain' },
|
||||
body: JSON.stringify({ password: 'normal-password' }),
|
||||
});
|
||||
|
||||
expect(accepted.status).toBe(200);
|
||||
expect(rejected.status).toBe(415);
|
||||
});
|
||||
|
||||
it('never treats a query parameter as a bearer credential', () => {
|
||||
expect(
|
||||
extractWebuiSessionToken(new Request('http://localhost/api/state/stream?token=leaked')),
|
||||
).toBe('');
|
||||
expect(
|
||||
extractWebuiSessionToken(
|
||||
new Request('http://localhost/api/state/stream', {
|
||||
headers: { authorization: 'Bearer header-token' },
|
||||
}),
|
||||
),
|
||||
).toBe('header-token');
|
||||
});
|
||||
|
||||
it('reads the WebUI session without consuming reverse proxy Basic credentials', () => {
|
||||
const request = new Request('http://localhost/api/auth/state', {
|
||||
headers: {
|
||||
authorization: 'Basic cHJveHk6cGFzc3dvcmQ=',
|
||||
'X-SnowLuma-Token': 'session-token',
|
||||
},
|
||||
});
|
||||
expect(extractWebuiSessionToken(request)).toBe('session-token');
|
||||
});
|
||||
|
||||
it.each(['session-token', 'invalid-token', ''])(
|
||||
'uses an explicit WebUI credential even when it is %j',
|
||||
(token) => {
|
||||
const request = new Request('http://localhost/api/auth/state', {
|
||||
headers: {
|
||||
authorization: 'Bearer legacy-token',
|
||||
'X-SnowLuma-Token': token,
|
||||
},
|
||||
});
|
||||
expect(extractWebuiSessionToken(request)).toBe(token);
|
||||
},
|
||||
);
|
||||
|
||||
it('does not treat proxy credentials or the avatar cookie as an API session', () => {
|
||||
const request = new Request('http://localhost/api/auth/state?token=leaked', {
|
||||
headers: {
|
||||
authorization: 'Basic cHJveHk6cGFzc3dvcmQ=',
|
||||
cookie: 'snowluma_avatar_session=avatar-token',
|
||||
},
|
||||
});
|
||||
expect(extractWebuiSessionToken(request)).toBe('');
|
||||
});
|
||||
|
||||
it('round-trips an HttpOnly avatar session cookie and clears it on logout', async () => {
|
||||
const app = new Hono();
|
||||
app.get('/set', (c) => {
|
||||
setAvatarSessionCookie(c, 'session-token', false);
|
||||
return c.text('ok');
|
||||
});
|
||||
app.get('/read', (c) => c.text(readAvatarSessionToken(c)));
|
||||
app.get('/clear', (c) => {
|
||||
clearAvatarSessionCookie(c, false);
|
||||
return c.text('ok');
|
||||
});
|
||||
|
||||
const setResponse = await app.request('/set');
|
||||
const setCookie = setResponse.headers.get('set-cookie') ?? '';
|
||||
expect(setCookie).toContain('HttpOnly');
|
||||
expect(setCookie).toContain('SameSite=Strict');
|
||||
expect(setCookie).toContain('Path=/avatar');
|
||||
|
||||
const cookie = setCookie.split(';', 1)[0];
|
||||
const readResponse = await app.request('/read', { headers: { cookie } });
|
||||
expect(await readResponse.text()).toBe('session-token');
|
||||
|
||||
const clearResponse = await app.request('/clear');
|
||||
expect(clearResponse.headers.get('set-cookie')).toMatch(/Max-Age=0/i);
|
||||
});
|
||||
|
||||
it('adds security headers without imposing a script or style policy', async () => {
|
||||
const app = new Hono();
|
||||
app.use('*', webuiSecurityHeaders);
|
||||
app.get('/', (c) => c.html('<main>SnowLuma</main>'));
|
||||
|
||||
const response = await app.request('/');
|
||||
expect(response.headers.get('x-frame-options')).toBe('DENY');
|
||||
expect(response.headers.get('x-content-type-options')).toBe('nosniff');
|
||||
expect(response.headers.get('referrer-policy')).toBe('no-referrer');
|
||||
expect(response.headers.get('content-security-policy')).toBe(
|
||||
"frame-ancestors 'none'; base-uri 'self'; object-src 'none'",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user