url-security.test.mjs 1.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
  1. import test from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import {
  4. hostMatchesAllowlist,
  5. isPrivateOrReservedAddress,
  6. validateRemoteAudioUrl,
  7. } from '../src/url-security.mjs';
  8. test('音频域名必须匹配显式白名单', () => {
  9. assert.equal(hostMatchesAllowlist('file.yuban.co', ['file.yuban.co']), true);
  10. assert.equal(hostMatchesAllowlist('a.media.example.com', ['*.media.example.com']), true);
  11. assert.equal(hostMatchesAllowlist('media.example.com', ['*.media.example.com']), false);
  12. assert.equal(hostMatchesAllowlist('attacker.example', ['file.yuban.co']), false);
  13. });
  14. test('阻止私网、回环和保留地址', () => {
  15. for (const address of ['127.0.0.1', '10.0.0.1', '172.16.0.1', '192.168.1.1', '::1', 'fd00::1']) {
  16. assert.equal(isPrivateOrReservedAddress(address), true, address);
  17. }
  18. assert.equal(isPrivateOrReservedAddress('1.1.1.1'), false);
  19. assert.equal(isPrivateOrReservedAddress('2606:4700:4700::1111'), false);
  20. });
  21. test('允许白名单 HTTPS 公网地址', async () => {
  22. const url = await validateRemoteAudioUrl(
  23. 'https://file.yuban.co/audio/test.mp3',
  24. ['file.yuban.co'],
  25. async () => [{ address: '1.1.1.1', family: 4 }],
  26. );
  27. assert.equal(url.hostname, 'file.yuban.co');
  28. });
  29. test('拒绝非 HTTPS 和私网解析', async () => {
  30. await assert.rejects(
  31. validateRemoteAudioUrl(
  32. 'http://file.yuban.co/test.mp3',
  33. ['file.yuban.co'],
  34. async () => [{ address: '1.1.1.1', family: 4 }],
  35. ),
  36. error => error.code === 'INVALID_AUDIO_URL',
  37. );
  38. await assert.rejects(
  39. validateRemoteAudioUrl(
  40. 'https://file.yuban.co/test.mp3',
  41. ['file.yuban.co'],
  42. async () => [{ address: '127.0.0.1', family: 4 }],
  43. ),
  44. error => error.code === 'AUDIO_ADDRESS_NOT_ALLOWED',
  45. );
  46. });