Files
Codeman/test/push-endpoint-validation.test.ts
T
arkonandClaude Opus 4.8 b84438a0aa fix(security): push-endpoint SSRF guard + tmux name validation; document tail-file roots
- M7 (SSRF): add isSafePushEndpoint (https-only; reject internal/loopback/link-local/metadata IPs incl. IPv4-mapped); enforce in PushSubscribeSchema and re-check before webpush.sendNotification. + unit test.
- M1 (command injection): validate tmux session names with isValidMuxName in sessionExists, killSession, and reconcileSessions before they reach a shell call site.
- M5: keep the intentional /var/log + ~/logs log-tail roots (a tested feature) and document the wider read scope in docs/security-architecture.md section 5 instead of dropping it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 20:02:17 +02:00

46 lines
2.0 KiB
TypeScript

/**
* SSRF guard for web-push endpoints (security review M7).
*/
import { describe, it, expect } from 'vitest';
import { isSafePushEndpoint } from '../src/utils/push-endpoint-validation.js';
describe('isSafePushEndpoint (SSRF guard, M7)', () => {
it('accepts real https push-service endpoints (public DNS hosts)', () => {
expect(isSafePushEndpoint('https://fcm.googleapis.com/fcm/send/abc123')).toBe(true);
expect(isSafePushEndpoint('https://updates.push.services.mozilla.com/wpush/v2/abc')).toBe(true);
expect(isSafePushEndpoint('https://web.push.apple.com/abc')).toBe(true);
expect(isSafePushEndpoint('https://foo.notify.windows.com/w/?token=x')).toBe(true);
});
it('accepts a public IP literal over https', () => {
expect(isSafePushEndpoint('https://93.184.216.34/x')).toBe(true);
});
it('rejects non-https schemes', () => {
expect(isSafePushEndpoint('http://fcm.googleapis.com/x')).toBe(false);
expect(isSafePushEndpoint('ftp://example.com/x')).toBe(false);
});
it('rejects the cloud-metadata IP and internal IPv4 ranges', () => {
expect(isSafePushEndpoint('https://169.254.169.254/latest/meta-data/')).toBe(false);
expect(isSafePushEndpoint('https://127.0.0.1/x')).toBe(false);
expect(isSafePushEndpoint('https://10.0.0.5/x')).toBe(false);
expect(isSafePushEndpoint('https://192.168.1.10/x')).toBe(false);
expect(isSafePushEndpoint('https://172.16.0.1/x')).toBe(false);
expect(isSafePushEndpoint('https://100.64.0.1/x')).toBe(false);
expect(isSafePushEndpoint('https://0.0.0.0/x')).toBe(false);
});
it('rejects internal IPv6 (incl. bracketed + IPv4-mapped)', () => {
expect(isSafePushEndpoint('https://[::1]/x')).toBe(false);
expect(isSafePushEndpoint('https://[fe80::1]/x')).toBe(false);
expect(isSafePushEndpoint('https://[fd00::1]/x')).toBe(false);
expect(isSafePushEndpoint('https://[::ffff:127.0.0.1]/x')).toBe(false);
});
it('rejects garbage / empty input', () => {
expect(isSafePushEndpoint('not a url')).toBe(false);
expect(isSafePushEndpoint('')).toBe(false);
});
});