Files
readest/apps/readest-app/src/__tests__/utils/network.test.ts
T
Huang Xin 184de9210d fix(security): prevent SSRF in kosync proxy (CWE-918) (#3793)
Validate serverUrl in the kosync API proxy to block requests to
private/internal addresses and non-http(s) schemes. Also fix isLanAddress
to detect 0.0.0.0 and bracket-wrapped IPv6 private addresses.

Closes code-scanning alert #14.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 21:21:51 +02:00

127 lines
5.1 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { isLanAddress } from '@/utils/network';
describe('isLanAddress', () => {
// -----------------------------------------------------------------------
// Localhost
// -----------------------------------------------------------------------
describe('localhost', () => {
it('returns true for localhost', () => {
expect(isLanAddress('http://localhost')).toBe(true);
expect(isLanAddress('http://localhost:8080')).toBe(true);
expect(isLanAddress('https://localhost/path')).toBe(true);
});
it('returns true for 127.0.0.1', () => {
expect(isLanAddress('http://127.0.0.1')).toBe(true);
expect(isLanAddress('http://127.0.0.1:3000')).toBe(true);
});
});
// -----------------------------------------------------------------------
// Private IPv4 ranges
// -----------------------------------------------------------------------
describe('private IPv4 ranges', () => {
it('returns true for 10.x.x.x (Class A)', () => {
expect(isLanAddress('http://10.0.0.1')).toBe(true);
expect(isLanAddress('http://10.255.255.255')).toBe(true);
expect(isLanAddress('http://10.1.2.3:8080')).toBe(true);
});
it('returns true for 172.16.x.x - 172.31.x.x (Class B)', () => {
expect(isLanAddress('http://172.16.0.1')).toBe(true);
expect(isLanAddress('http://172.31.255.255')).toBe(true);
expect(isLanAddress('http://172.20.1.1')).toBe(true);
});
it('returns false for 172.x.x.x outside the private range', () => {
expect(isLanAddress('http://172.15.0.1')).toBe(false);
expect(isLanAddress('http://172.32.0.1')).toBe(false);
});
it('returns true for 192.168.x.x (Class C)', () => {
expect(isLanAddress('http://192.168.0.1')).toBe(true);
expect(isLanAddress('http://192.168.1.100:9090')).toBe(true);
expect(isLanAddress('http://192.168.255.255')).toBe(true);
});
});
// -----------------------------------------------------------------------
// Link-local addresses
// -----------------------------------------------------------------------
describe('link-local addresses', () => {
it('returns true for 169.254.x.x', () => {
expect(isLanAddress('http://169.254.0.1')).toBe(true);
expect(isLanAddress('http://169.254.255.255')).toBe(true);
});
});
// -----------------------------------------------------------------------
// Tailscale range
// -----------------------------------------------------------------------
describe('Tailscale CGNAT range', () => {
it('returns true for 100.64.x.x - 100.127.x.x', () => {
expect(isLanAddress('http://100.64.0.1')).toBe(true);
expect(isLanAddress('http://100.127.255.255')).toBe(true);
expect(isLanAddress('http://100.100.100.100')).toBe(true);
});
it('returns false for 100.x.x.x outside Tailscale range', () => {
expect(isLanAddress('http://100.63.0.1')).toBe(false);
expect(isLanAddress('http://100.128.0.1')).toBe(false);
});
});
// -----------------------------------------------------------------------
// IPv6 private addresses
// -----------------------------------------------------------------------
describe('IPv6 private addresses', () => {
it('returns true for bracket-wrapped IPv6 loopback', () => {
expect(isLanAddress('http://[::1]')).toBe(true);
expect(isLanAddress('http://[::1]:8080')).toBe(true);
});
it('returns true for bracket-wrapped IPv6 link-local and unique local', () => {
expect(isLanAddress('http://[fe80::1]')).toBe(true);
expect(isLanAddress('http://[fc00::1]')).toBe(true);
expect(isLanAddress('http://[fd00::abc]')).toBe(true);
});
});
// -----------------------------------------------------------------------
// Public addresses
// -----------------------------------------------------------------------
describe('public addresses', () => {
it('returns false for public IPv4', () => {
expect(isLanAddress('http://8.8.8.8')).toBe(false);
expect(isLanAddress('http://1.1.1.1')).toBe(false);
expect(isLanAddress('http://203.0.113.50')).toBe(false);
});
it('returns false for domain names', () => {
expect(isLanAddress('http://example.com')).toBe(false);
expect(isLanAddress('https://google.com')).toBe(false);
expect(isLanAddress('http://my-server.local.example.com')).toBe(false);
});
});
// -----------------------------------------------------------------------
// Invalid / edge cases
// -----------------------------------------------------------------------
describe('invalid / edge cases', () => {
it('returns false for invalid IP octets > 255', () => {
expect(isLanAddress('http://10.256.0.1')).toBe(false);
expect(isLanAddress('http://192.168.1.999')).toBe(false);
});
it('returns false for invalid URL', () => {
expect(isLanAddress('not-a-url')).toBe(false);
expect(isLanAddress('')).toBe(false);
});
it('returns false for non-IPv6-private addresses', () => {
expect(isLanAddress('http://[2001:db8::1]')).toBe(false);
});
});
});