Passing `username` from URL object to http.clientRequest without decoding

  • Version: v8.x.x+
  • Platform: any
  • Subsystem: any

Description

Passing username with "unsafe" symbols (e.g. @) to URL object causes wrongly computed Basic-Authorization header string.

Pre-requisites

The next code looks good enough (Node.js CLI):

const {URL} = require('url');
const url = new URL('http://localhost');
url.username = 'test@test';
url.password = '123456';
console.log(url);

This should result in:

URL {
  href: 'http://test%40test:123456@localhost/',
  origin: 'http://localhost',
  protocol: 'http:',
  username: 'test%40test',
  password: '123456',
  host: 'localhost',
  hostname: 'localhost',
  port: '',
  pathname: '/',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
}

The field username turned to percent-encoded as mentioned in the documentation (https://nodejs.org/api/url.html#url_url_username). According to the composed URI in the field href it's working as expected.

Expected behavior

Reference calls via cURL will look like:

curl -u 'test@test:123456' -v 'http://localhost/'
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 80 (#0)
* Server auth using Basic with user 'test@test'
> GET / HTTP/1.1
> Host: localhost
> Authorization: Basic dGVzdEB0ZXN0OjEyMzQ1Ng==
> User-Agent: curl/7.58.0
> Accept: */*

curl -v 'http://test%40test:123456@localhost/'
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 80 (#0)
* Server auth using Basic with user 'test@test'
> GET / HTTP/1.1
> Host: localhost
> Authorization: Basic dGVzdEB0ZXN0OjEyMzQ1Ng==
> User-Agent: curl/7.58.0
> Accept: */*

Decoding the header Authorization: Basic dGVzdEB0ZXN0OjEyMzQ1Ng== results to test@test:123456 as expected.

Actual behavior

Again try to make the same call from Node.js CLI:

const {URL} = require('url');
const url = new URL('http://localhost');
url.username = 'test@test';
url.password = '123456';

const http = require('http');
console.log(http.get(url, () => {}).outputData);

That will output something like:

[
  {
    data: 'GET /profile HTTP/1.1\r\n' +
      'Host: localhost\r\n' +
      'Authorization: Basic dGVzdCU0MHRlc3Q6MTIzNDU2\r\n' +
      'Connection: close\r\n' +
      '\r\n',
    encoding: 'latin1',
    callback: [Function: bound onFinish]
  }
]

Decoding Authorization header results to test%40test:123456, which is wrong.

Expectation

When http.request(<URL>) grabs a value from href or username fields, it should sanitize and decode values before composing Authorization header.

-or-

WHATWG-URL should keep raw username and provide it like:

URL {
  href: 'http://test%40test:123456@localhost/', # here encoded values
  origin: 'http://localhost',
  protocol: 'http:',
  username: 'test@test', # here should be raw value
  password: '123456',
  host: 'localhost',
  hostname: 'localhost',
  port: '',
  pathname: '/',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
}

References