forked from akai/readest
184de9210d
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>
127 lines
5.1 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|