UNPKG

fast-uri

Version:

Dependency-free RFC 3986 URI toolbox

138 lines (121 loc) • 5.04 kB
'use strict' const test = require('tape') const fastURI = require('..') const PATH_RESERVED = "!$&'()*+,;=:@/" function percentEncode (character, lowerCase) { const hex = character.charCodeAt(0).toString(16).padStart(2, '0') return '%' + (lowerCase ? hex : hex.toUpperCase()) } test('normalize preserves literal and escaped reserved path characters', (t) => { t.equal( fastURI.normalize('http://example.com/a;b'), 'http://example.com/a;b', 'literal semicolon remains literal' ) t.equal( fastURI.normalize('http://example.com/a%3ab'), 'http://example.com/a%3Ab', 'escaped colon remains escaped and its hex is uppercased' ) for (const character of PATH_RESERVED) { const literal = `http://example.com/a${character}b` const escaped = `http://example.com/a${percentEncode(character, true)}b` const normalizedEscape = `http://example.com/a${percentEncode(character, false)}b` t.equal(fastURI.normalize(literal), literal, `preserves literal ${character}`) t.equal(fastURI.normalize(escaped), normalizedEscape, `preserves escaped ${character}`) } t.equal( fastURI.normalize('http://example.com/a/./café'), 'http://example.com/a/caf%C3%A9', 'removes real dot segments and UTF-8 encodes raw non-ASCII' ) t.equal( fastURI.normalize('http://example.com/a/%2e%2e/b'), 'http://example.com/a/%2E%2E/b', 'preserves escaped dots as path data' ) t.equal( fastURI.normalize('http://example.com/Kſ'), 'http://example.com/%E2%84%AA%C5%BF', 'UTF-8 encodes Unicode characters that case-fold to ASCII' ) t.end() }) test('serialize uses the RFC 3986 path character set without opening escapes', (t) => { const rawPath = `/a${PATH_RESERVED}b` t.equal( fastURI.serialize({ scheme: 'http', host: 'example.com', path: rawPath }), `http://example.com${rawPath}`, 'keeps all legal raw path characters' ) const escapedPath = Array.from(PATH_RESERVED, (character) => percentEncode(character, true)).join('') const normalizedEscapedPath = Array.from(PATH_RESERVED, (character) => percentEncode(character, false)).join('') t.equal( fastURI.serialize({ scheme: 'http', host: 'example.com', path: '/' + escapedPath }), 'http://example.com/' + normalizedEscapedPath, 'uppercases valid escapes without decoding reserved characters' ) t.equal( fastURI.serialize({ scheme: 'http', host: 'example.com', path: '/a?b#c[d]' }), 'http://example.com/a%3Fb%23c%5Bd%5D', 'encodes characters that would leave the path component' ) t.equal( fastURI.serialize({ path: 'a:b/c:d' }), 'a%3Ab/c:d', 'only escapes a colon where path-noscheme requires it' ) t.equal( fastURI.serialize({ path: './a:b/c:d' }), './a:b/c:d', 'a first-segment colon exposed by dot-segment removal is kept behind a "." segment' ) t.equal( fastURI.normalize('./a:b'), './a:b', 'normalization keeps a relative path from becoming a scheme without re-encoding the colon' ) t.equal( fastURI.serialize({ path: '/Kſ' }), '/%E2%84%AA%C5%BF', 'UTF-8 encodes Unicode characters that case-fold to ASCII' ) t.end() }) test('equal distinguishes escaped reserved path data from literal syntax', (t) => { for (const character of PATH_RESERVED) { const literal = `http://example.com/a${character}b` const escaped = `http://example.com/a${percentEncode(character, false)}b` t.equal(fastURI.equal(literal, escaped, {}), false, `distinguishes literal and escaped ${character}`) } t.equal( fastURI.equal('./a:b', 'a:b', {}), false, 'does not equate a relative path with an absolute URI after removing dot segments' ) t.end() }) // A suffix reference never emits its scheme token, so it is a scheme-less // (path-noscheme) reference: a bare ':' in its first path segment must be // escaped, or the serialized output reparses as an absolute URI with a scheme. // @see https://github.com/fastify/fast-uri/security/advisories/GHSA-333x-w4r6-6crp test('suffix reference escapes a first-segment colon instead of emitting a scheme', (t) => { const dangerous = /^(?:javascript|data|vbscript):/i const options = { reference: 'suffix', scheme: 'x' } for (const input of ['javascript:alert(1)', '%6Aavascript:alert(1)', 'data:text/html,x', 'vbscript:msgbox(1)']) { t.notOk(dangerous.test(fastURI.normalize(input, options)), `${input} normalizes to an inert reference`) } t.equal( fastURI.normalize('javascript:alert(1)', options), 'javascript%3Aalert(1)', 'escapes the leading-segment colon to %3A' ) t.notOk( dangerous.test(fastURI.resolve('https://a.example/base/', '%6Aavascript:alert(1)', { reference: 'suffix' })), 'resolve in suffix mode does not emit a live scheme' ) // legitimate suffix usage is unchanged t.equal(fastURI.normalize('path/to/file', options), 'path/to/file', 'plain path unchanged') t.equal(fastURI.normalize('a/b:c', options), 'a/b:c', 'colon in a later segment stays literal') t.end() })