Fixed invalid path in CONNECT method requests. - #34412
Conversation
|
@szmarczak could you have a look at this? |
There was a problem hiding this comment.
Should options.path be required when the method is CONNECT?
There was a problem hiding this comment.
Two things:
-
Don't modify
options.path- theoptionsobject can be owned by the caller, not by this method; mutations will be visible. -
CONNECTrequires 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.
There was a problem hiding this comment.
Should
options.pathbe required when the method isCONNECT?
@szmarczak
Apparently yes, there was a test in which options.path is undefined so charAt throws an error.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
|
Gentle ping @bnoordhuis |
There was a problem hiding this comment.
Does this sound good? Maybe validateConnectPath? Or validateAuthority?
There was a problem hiding this comment.
Why not
| if (method === 'CONNECT' && this.path !== '/') { | |
| if (method === 'CONNECT') { |
There was a problem hiding this comment.
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 = '/';
}There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
@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 = '/';
}There was a problem hiding this comment.
Agreed, and as a micro-optimization: assign to a
let pathfirst and then dothis.path = path;outside theifblock.(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.
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
e7d3f83 to
c9f1597
Compare
bnoordhuis
left a comment
There was a problem hiding this comment.
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.
| const { kOutHeaders, | ||
| kNeedDrain, | ||
| isValidCONNECTPath | ||
| } = require('internal/http'); |
There was a problem hiding this comment.
| const { kOutHeaders, | |
| kNeedDrain, | |
| isValidCONNECTPath | |
| } = require('internal/http'); | |
| const { | |
| kOutHeaders, | |
| kNeedDrain, | |
| isValidCONNECTPath, | |
| } = require('internal/http'); |
| path = options.path; | ||
| if (path) { | ||
| if (method === 'CONNECT') { | ||
| if (path.charAt(0) === '/') { |
There was a problem hiding this comment.
| if (path.charAt(0) === '/') { | |
| if (path.startsWith('/')) { |
Arguably a little more idiomatic for core code.
| this.path = path; | ||
| } else { | ||
| this.path = path; | ||
| } | ||
| } else { | ||
| this.path = path; | ||
| } | ||
| } else { | ||
| this.path = '/'; | ||
| } |
There was a problem hiding this comment.
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. :-)
| if (!VALID_PATH_REGEX.test(path)) { | ||
| return false; | ||
| } | ||
| return true; |
There was a problem hiding this comment.
| if (!VALID_PATH_REGEX.test(path)) { | |
| return false; | |
| } | |
| return true; | |
| return VALID_PATH_REGEX.test(path)); |
| path = options.path; | ||
| if (path) { | ||
| if (method === 'CONNECT') { | ||
| if (path.startsWith('/')) { |
There was a problem hiding this comment.
path[0] === '/' is much faster
|
Ping @bnoordhuis |
| utcCache = undefined; | ||
| } | ||
|
|
||
| function isValidCONNECTPath(path) { |
There was a problem hiding this comment.
It might be better to use the Camel-Case, e.g. isValidConnectPath.
| 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])$/; |
There was a problem hiding this comment.
Benchmarks tests may be required to assess whether performance has been affected.
There was a problem hiding this comment.
@rickyes Could you guide me more about these benchmark tests, as how to write one?
There was a problem hiding this comment.
@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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Also you need to check that http://${url.host} is url.href to disallow asdf.com:1234/asdf etc.
|
This would need a rebase if we still want to land it. |
|
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. |
|
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. |
Fixes: #34347
Checklist
make -j4 test(UNIX), orvcbuild test(Windows) passes