| 'use strict' |
| |
| const test = require('tape') |
| const fastURI = require('..') |
| |
| test('userinfo serialization cannot terminate the authority', (t) => { |
| const uri = fastURI.serialize({ |
| scheme: 'http', |
| userinfo: 'attacker.example/', |
| host: 'trusted.example', |
| path: '/' |
| }) |
| |
| t.equal(uri, 'http://attacker.example%2F@trusted.example/', 'slash is encoded as userinfo data') |
| |
| const reparsed = fastURI.parse(uri) |
| t.equal(reparsed.host, 'trusted.example', 'reparsing retains the supplied host') |
| t.equal(reparsed.userinfo, 'attacker.example%2F', 'encoded slash remains in userinfo') |
| t.equal( |
| fastURI.serialize({ userinfo: 'user@/?:#[]\\', host: 'example.test' }), |
| '//user%40%2F%3F:%23%5B%5D%5C@example.test', |
| 'all userinfo component delimiters are encoded as data' |
| ) |
| t.end() |
| }) |
| |
| test('port serialization rejects non-digit values', (t) => { |
| const malformedPorts = [ |
| '@127.0.0.1:8124', |
| '8080@evil.example', |
| '8080/path', |
| '8080?query', |
| '8080#fragment', |
| '8080:9000', |
| '-1', |
| '1.5', |
| 1.5, |
| NaN, |
| Infinity, |
| '\u0661' |
| ] |
| |
| for (const port of malformedPorts) { |
| t.throws( |
| () => fastURI.serialize({ scheme: 'http', host: 'trusted.example', port, path: '/app' }), |
| /URI port is malformed\./, |
| String(port) |
| ) |
| } |
| |
| t.throws( |
| () => fastURI.normalize({ scheme: 'http', host: 'trusted.example', port: '@evil.example' }), |
| /URI port is malformed\./, |
| 'object normalization rejects a malformed port' |
| ) |
| t.equal( |
| fastURI.equal( |
| { scheme: 'http', host: 'trusted.example', port: '@evil.example' }, |
| { scheme: 'http', host: 'trusted.example', port: '@evil.example' } |
| ), |
| false, |
| 'object equality fails closed for a malformed port' |
| ) |
| t.end() |
| }) |
| |
| test('port serialization preserves RFC 3986 digit values', (t) => { |
| const validPorts = [ |
| [8080, '8080'], |
| ['8080', '8080'], |
| ['00080', '00080'], |
| ['', ''] |
| ] |
| |
| for (const [port, expected] of validPorts) { |
| t.equal( |
| fastURI.serialize({ scheme: 'uri', host: 'example.test', port }), |
| `uri://example.test:${expected}`, |
| JSON.stringify(port) |
| ) |
| } |
| t.end() |
| }) |
| |
| test('query and fragment serialization cannot inject component delimiters', (t) => { |
| const uri = fastURI.serialize({ |
| scheme: 'x', |
| path: '/resource', |
| query: 'key=value#still-query', |
| fragment: 'first#still-fragment' |
| }) |
| |
| t.equal( |
| uri, |
| 'x:/resource?key=value%23still-query#first%23still-fragment', |
| 'raw hashes are encoded within their supplied components' |
| ) |
| |
| const reparsed = fastURI.parse(uri) |
| t.equal(reparsed.query, 'key=value%23still-query', 'hash remains query data') |
| t.equal(reparsed.fragment, 'first%23still-fragment', 'hash remains fragment data') |
| t.end() |
| }) |
| |
| test('component serializers preserve each RFC 3986 literal character set', (t) => { |
| const unreservedAndSubDelims = "AZaz09-._~!$&'()*+,;=" |
| const userinfo = unreservedAndSubDelims + ':' |
| const queryOrFragment = unreservedAndSubDelims + ':@/?' |
| |
| t.equal( |
| fastURI.serialize({ userinfo, host: 'example.test' }), |
| '//' + userinfo + '@example.test', |
| 'userinfo literals are preserved' |
| ) |
| t.equal( |
| fastURI.serialize({ query: queryOrFragment }), |
| '?' + queryOrFragment, |
| 'query literals are preserved' |
| ) |
| t.equal( |
| fastURI.serialize({ fragment: queryOrFragment }), |
| '#' + queryOrFragment, |
| 'fragment literals are preserved' |
| ) |
| t.end() |
| }) |
| |
| test('component serializers preserve escapes and encode Unicode as UTF-8', (t) => { |
| t.equal( |
| fastURI.serialize({ userinfo: 'café/%2f%GG', host: 'example.test' }), |
| '//caf%C3%A9%2F%2F%25GG@example.test', |
| 'userinfo preserves valid escapes, uppercases hex, and escapes malformed percent data' |
| ) |
| t.equal( |
| fastURI.serialize({ query: '日本/%2f#%GG' }), |
| '?%E6%97%A5%E6%9C%AC/%2F%23%25GG', |
| 'query uses UTF-8 and does not double encode valid escapes' |
| ) |
| t.equal( |
| fastURI.serialize({ fragment: '😀/%3f#%GG' }), |
| '#%F0%9F%98%80/%3F%23%25GG', |
| 'fragment uses UTF-8 and does not double encode valid escapes' |
| ) |
| t.equal( |
| fastURI.serialize({ query: 'Kſ' }), |
| '?%E2%84%AA%C5%BF', |
| 'Unicode characters that case-fold to ASCII are still UTF-8 encoded' |
| ) |
| t.end() |
| }) |
| |
| test('parsed URI serialization remains stable', (t) => { |
| const input = 'x://user:pass@example.test/a%2Fb?x=a/b?c&y=%23#frag/a?b%23' |
| const serialized = fastURI.serialize(fastURI.parse(input)) |
| |
| t.equal(serialized, input, 'an already normalized parsed URI round-trips unchanged') |
| t.equal( |
| fastURI.serialize(fastURI.parse(serialized)), |
| serialized, |
| 'repeated parse and serialize cycles are stable' |
| ) |
| t.end() |
| }) |