Skip to content

Fixed invalid path in CONNECT method requests. - #34412

Closed
preyunk wants to merge 9 commits into
nodejs:mainfrom
preyunk:connect-pathname
Closed

Fixed invalid path in CONNECT method requests.#34412
preyunk wants to merge 9 commits into
nodejs:mainfrom
preyunk:connect-pathname

Conversation

@preyunk

@preyunk preyunk commented Jul 17, 2020

Copy link
Copy Markdown
Contributor

Fixes: #34347

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines

@nodejs-github-bot nodejs-github-bot added the http Issues or PRs related to the http subsystem. label Jul 17, 2020
@preyunk

preyunk commented Jul 17, 2020

Copy link
Copy Markdown
Contributor Author

@szmarczak could you have a look at this?

Comment thread test/parallel/test-http-request-connect-method.js Outdated
Comment thread lib/_http_client.js Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should options.path be required when the method is CONNECT?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things:

  1. Don't modify options.path - the options object can be owned by the caller, not by this method; mutations will be visible.

  2. CONNECT requires a <host>:<port> combo. Chopping off the leading slash is the first step but it should also check it's a valid combo.

I don't think we have a utility function for that right now and checking if new URL(`http://${options.path}`) throws isn't robust (accepts e.g. evil.com:666/good.org:777.)

I think the regex would look something like this...

^[\-_0-9A-Za-z]+(\.[\-_0-9A-Za-z]+)*\.?:([1-9]|[1-9][0-9]{1,2}|[1-5][0-9]{3}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$

(I hope I'm not introducing catastrophic backtracing here...)

It should match inputs like example.com:80 but also example.com.:80 (FQDN), example:80, example.:80, etc., but not e.g. example.com:0 or example.com:65536.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should options.path be required when the method is CONNECT?

@szmarczak
Apparently yes, there was a test in which options.path is undefined so charAt throws an error.

@preyunk preyunk Jul 18, 2020

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bnoordhuis

Don't modify options.path - the options object can be owned by the caller, not by this method; mutations will be visible.

Okay, I'll try avoiding that.

I don't think we have a utility function for that right now and checking if new URL(http://${options.path}) throws isn't robust (accepts e.g. evil.com:666/good.org:777.)

Should we include this check as a utility function in url.js or must check for a valid <host>:<port> here in _http_client.js?
Also it would require some additional tests right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It certainly shouldn't be exposed as a public function, if that is what you're asking.

An internal function in _http_client.js is okay but if you add it to lib/internal/http.js, you can unit-test it if you add a // Flags: --expose-internals magic comment to the test file.

@preyunk
preyunk requested review from bnoordhuis and jasnell July 21, 2020 20:59
Comment thread doc/api/errors.md Outdated
Comment thread test/parallel/test-http-request-connect-method.js Outdated
@preyunk
preyunk requested a review from jasnell July 22, 2020 14:52
jasnell
jasnell previously approved these changes Jul 22, 2020
@preyunk

preyunk commented Jul 25, 2020

Copy link
Copy Markdown
Contributor Author

Gentle ping @bnoordhuis

Comment thread lib/internal/http.js Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this sound good? Maybe validateConnectPath? Or validateAuthority?

Comment thread lib/_http_client.js Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not

Suggested change
if (method === 'CONNECT' && this.path !== '/') {
if (method === 'CONNECT') {

Comment thread lib/_http_client.js Outdated

@szmarczak szmarczak Jul 26, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is much cleaner:

  if (options.path) {
    if (method === 'CONNECT') {
      if (options.path.charAt(0) === '/') {
        this.path = options.path.slice(1);
      } else {
        this.path = options.path;
      }
    } else {
      this.path = options.path;
    }
  } else {
    this.path = '/';
  }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and as a micro-optimization: assign to a let path first and then do this.path = path; outside the if block.

(Ask me why if you want details but it has to do with how V8 stores object properties.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bnoordhuis Do you mean this?

  const {path} = options;
  if (path) {
    if (method === 'CONNECT') {
      if (path.charAt(0) === '/') {
        this.path = path.slice(1);
      } else {
        this.path = path;
      }
    } else {
      this.path = path;
    }
  } else {
    this.path = '/';
  }

@preyunk preyunk Jul 28, 2020

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and as a micro-optimization: assign to a let path first and then do this.path = path; outside the if block.

(Ask me why if you want details but it has to do with how V8 stores object properties.)

Yes, I was about to ask the reason for this, could you elaborate as why we are avoiding the direct use of options.path?

const {path} = options;

There is already a let path variable declared in _http_client.js. I think it must be reused.

Comment thread lib/_http_client.js Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and as a micro-optimization: assign to a let path first and then do this.path = path; outside the if block.

(Ask me why if you want details but it has to do with how V8 stores object properties.)

Comment thread lib/internal/http.js Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth checking if it matters performance-wise when you move the regex outside the function.

The regex should use (?:...) non-capturing groups, otherwise it pollutes RegExp.$1, etc.

Comment thread lib/internal/http.js Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd change this to simply return true/false and have the caller throw the exception (and rename to e.g. isValidCONNECTPath().)

Rule of thumb: exceptions should be thrown close to the edge, where "edge" means the cross-over point between user code and core code.

@preyunk
preyunk requested a review from bnoordhuis August 5, 2020 15:11

@bnoordhuis bnoordhuis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments but mostly LGTM.

@jasnell I'm going to dismiss your review because this PR has gone through substantial changes and I think it needs at least one other review before it can land.

Comment thread lib/_http_client.js Outdated
Comment on lines +52 to +55
const { kOutHeaders,
kNeedDrain,
isValidCONNECTPath
} = require('internal/http');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const { kOutHeaders,
kNeedDrain,
isValidCONNECTPath
} = require('internal/http');
const {
kOutHeaders,
kNeedDrain,
isValidCONNECTPath,
} = require('internal/http');

Comment thread lib/_http_client.js Outdated
path = options.path;
if (path) {
if (method === 'CONNECT') {
if (path.charAt(0) === '/') {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (path.charAt(0) === '/') {
if (path.startsWith('/')) {

Arguably a little more idiomatic for core code.

Comment thread lib/_http_client.js Outdated
Comment on lines +214 to +223
this.path = path;
} else {
this.path = path;
}
} else {
this.path = path;
}
} else {
this.path = '/';
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you assign to this.path just once outside the if block? Every this.path = ... forces V8 to emit a type check that this is an object of the expected shape.

V8 can hoist and coalesce such checks but... it's complicated. Just assign once, please. :-)

Comment thread lib/internal/http.js Outdated
Comment on lines +37 to +40
if (!VALID_PATH_REGEX.test(path)) {
return false;
}
return true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!VALID_PATH_REGEX.test(path)) {
return false;
}
return true;
return VALID_PATH_REGEX.test(path));

@bnoordhuis
bnoordhuis dismissed jasnell’s stale review August 6, 2020 08:34

for old version of pr

@preyunk
preyunk requested review from bnoordhuis and jasnell August 6, 2020 10:33
Comment thread lib/_http_client.js
path = options.path;
if (path) {
if (method === 'CONNECT') {
if (path.startsWith('/')) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path[0] === '/' is much faster

@preyunk
preyunk requested a review from a team as a code owner August 10, 2020 16:06
@preyunk

preyunk commented Aug 16, 2020

Copy link
Copy Markdown
Contributor Author

Ping @bnoordhuis

Comment thread lib/internal/http.js Outdated
utcCache = undefined;
}

function isValidCONNECTPath(path) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better to use the Camel-Case, e.g. isValidConnectPath.

Comment thread lib/internal/http.js Outdated
const { setUnrefTimeout } = require('internal/timers');
const { PerformanceEntry, notify } = internalBinding('performance');

const VALID_PATH_REGEX = /^[_0-9A-Za-z]+(?:\.[_0-9A-Za-z]+)*\.?:(?:[1-9]|[1-9][0-9]{1,2}|[1-5][0-9]{3}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmarks tests may be required to assess whether performance has been affected.

@preyunk preyunk Aug 19, 2020

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rickyes Could you guide me more about these benchmark tests, as how to write one?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@preyunk Usually there is no need to write additional benchmarks code, you can use existing benchmarks for testing, refer to the document https://github.com/nodejs/node/blob/master/doc/guides/writing-and-running-benchmarks.md

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex here does not properly account for the possibility of IPv6 addresses. For instance, [2001:db8::1]:111 should be a valid path but fails this check. A likely better approach for the validation check would be to attempt to create a URL from the path:

new URL(`http://${path}`)

If creating the URL is successful, check that the hostname and port are appropriately set, then continue.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also you need to check that http://${url.host} is url.href to disallow asdf.com:1234/asdf etc.

@preyunk
preyunk requested review from rickyes and removed request for a team September 6, 2020 17:38
@aduh95

aduh95 commented Sep 19, 2023

Copy link
Copy Markdown
Contributor

This would need a rebase if we still want to land it.

@aduh95 aduh95 added the stalled Issues and PRs that are stalled. label Sep 19, 2023
@github-actions

Copy link
Copy Markdown
Contributor

This issue/PR was marked as stalled, it will be automatically closed in 30 days. If it should remain open, please leave a comment explaining why it should remain open.

@github-actions

Copy link
Copy Markdown
Contributor

Closing this because it has stalled. Feel free to reopen if this issue/PR is still relevant, or to ping the collaborator who labelled it stalled if you have any questions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

http Issues or PRs related to the http subsystem. stalled Issues and PRs that are stalled.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Passing a URL instance with a CONNECT method results in an invalid path

7 participants