Open ronjouch opened 1 year ago
This module does not claim to have this protection in any way.
I think it should be possible to implement this with an interceptor: https://github.com/nodejs/undici/blob/main/docs/api/DispatchInterceptor.md.
We might want to develop some mitigation via a custom agent. wdyt @RafaelGSS @ronag?
@mcollina thx for the fast reply!
This module does not claim to have this protection in any way.
Okay. Not surprised, given that fetch
comes foremost from a browser world.
I think it should be possible to implement this with an interceptor: https://github.com/nodejs/undici/blob/main/docs/api/DispatchInterceptor.md . We might want to develop some mitigation via a custom agent.
👍, super interested in this discussion. But in an interceptor world, if I read correctly your types at
https://github.com/nodejs/undici/blob/7276126945c52cf7ec61460b36d19f882e1bab82/types/dispatcher.d.ts#L96-L98
, I see that a DispatchInterceptor
is being fed a DispatchOptions
with an origin
URL and path
, whereas I need a host/IP. So, from my understanding, I'd still have to do my own dns lookup here (which undici will repeat afterwards), which means this API isn't exactly set up at the right point I need in the request lifecycle (after { DNS resolution, redirects followup }, before firing the request) 😕. Am I correct?
In the meantime, and putting aside interceptors, my plan was to do the SSRF check manually before calling fetch
, repeating what ssrf-req-filter does with help from ipaddr.js:
const ip = await require('dns').promises.lookup(maybeMaliciousUrl).address;
const isSuspicious = require('ipaddr.js').parse(ip).range() !== 'unicast';
if (isSuspicious) { throw 'why u ssrf'; }
I actually like this thing, it's straightforward and explicit, more than the plugins.
However, I dislike that it forces an extra dns lookup, whereas a "plugin" agent solution integrated to fetch
(similar to what request & axios do) does the lookup, and feeds it the agent/interceptor the IP, eliminating the extra dns resolution.
Any opinion/comment about it? Or should I just not care because the dns lookup fast/cached?
I think we might have to provide a checkup option in Client
for you to plug into.
Would you like to send a Pull Request to address this issue? Remember to add unit tests.
I think we might have to provide a checkup option in
Client
for you to plug into.
@mcollina not sure I understand you: what do you mean by a checkup option in Client
?
Would you like to send a Pull Request to address this issue? Remember to add unit tests.
@mcollina considering it. As mentioned in https://github.com/nodejs/undici/issues/2019#issuecomment-1478534997 , I except for my DNS resolve question I also somehow like a dumb pre-call check (wrappable in a helper function). But I don't fully understand yet how e.g. request
does its agent
thing, and if it's anymore DNS-lookup-efficient than a dumb pre-call check.
I'll be researching what request
does, and if still convinced by the agent
approach, will give a PR a try. No commitment as long as I have no code to show, and passersby very welcome to jump on it and do it, of course.
Thanks for your halp.
By looking at how ssrf-req-filter is implemented, we need to add an option to Client
so that we can install the same lookup
event handler inside our connect
function.
@ronjouch
I actually like this thing, it's straightforward and explicit, more than the plugins.
However, I dislike that it forces an extra dns lookup, whereas a "plugin" agent solution integrated to
fetch
(similar to what request & axios do) does the lookup, and feeds it the agent/interceptor the IP, eliminating the extra dns resolution.Any opinion/comment about it? Or should I just not care because the dns lookup fast/cached?
This is actually insecure because of a TOCTOU (time of check, time of use) vulnerability (owasp link). You can create e.g. a round robin A record where only one of the addresses is for an internal IP while the others are fine. So when you check you get one IP and when you run it you get another.
FWIW I was able to pass a custom lookup
function to get this functionality:
import dns from 'node:dns';
const lookup = function (hostname, options, callback) {
dns.lookup(hostname, options, (err, address, family) => {
if (err) {
return callback(err, address, family);
}
if (address === '127.0.0.1') {
return callback(new Error('not allowed'), address, family);
}
return callback(err, address, family);
})
}
const agent = new Agent({
connect: {
lookup
}
});
setGlobalDispatcher(agent);
@nunofgs
FWIW I was able to pass a custom lookup function to get this functionality:
Using a global dispatcher might be problematic as, in some cases, you do might want to be able to contact other micro services. Also, using only the lookup
function as protection would allow an attacker to circumvent the protection by using an IP in the URI (e.g. http://[::1]/foo
).
I personally use a wrapper around fetch that can be used as follows:
const safeFetch = ssrfFetchWrap({ fetch: globalThis.fetch })
safeFetch(new Request('http://[::1]/foo')) // Will throw
fetch()
can still be used for trusted hosts (e.g. micro services) and safeFetch()
for user provided endpoints.
@matthieusieben see my previous comment https://github.com/nodejs/undici/issues/2019#issuecomment-1706572083 as to why this is unsafe.
This would solve...
undici
and native-node>=18-fetch
expose (as far as I know, and see documented) no way to protect against SSRF attacks when making server-side calls to arbitrary / user-controllable URLs (that are normally WWW, but could be abused to try to exfiltrate info from an internal network).In the nodejs ecosystem,
fetch
libs likerequest
oraxios
can use middleware like ssrf-req-filter. It provides anagent
/httpAgent
/httpsAgent
that you can pass in these libs secondoptions
argument:fetch
libnode-fetch
also supports ssrf-req-filter, though a similaragent
API:The implementation of such SSRF protection is out of topic, but:
Given the above, my question:
In
undici
/ built-in-node>=18 nativefetch
, I see no trace of supporting such agents with a({host: string}) => boolean
signature letting me decide at runtime to cancel a request.fetch
? (At the cost of an extra DNS resolution before callingfetch
)fetch
at some point?The implementation should look like...
undici (fetch?) should support setting kind of option letting users plug anti-SSRF logic.
Typically, an
agent
function of signature{host: string} => boolean
(agent is passed an IP, and expected to return a boolean).I have also considered...
I searched undici's open & closed issues for
ssrf
, nothing. Grepping foragent
yields many things but it looks like there's a naming collision around what is anagent
in your context vs. in an axios/request context. They don't seem to have similar responsibilities.I searched undici's discussions for
ssrf
, nothing.I read the options supported by standard
fetch
in mdn / fetch, nothing in the documented options I could find related toagent
/ssrf
stuff.I grepped the fetch WHATWG spec for "SSRF", without success.
Additional context
Related to CVE-2023-28155 / request #3442
Thanks for fetch!