Mitigating SSRF vulnerabilities in Go. A practical guide. Part 2
In this final part of mitigation guide we will explore doyensec/safeurl library for Go.
Setting the stage
Reminder about our setup:
Explot 3
We ended up previous part with following Public API code and exploit that works:
router.GET("/debug", func(context *gin.Context) {
urlFromUser := context.Query("url")
// validation because world is full of mean people :(
if !validateTargetUrl(urlFromUser) {
context.String(http.StatusBadRequest, "Bad url")
return
}
resp, err := http.Get(urlFromUser)
$ curl -s \
http://publicapi/debug\?url\=
http://imageapi/redirect\?target\=
http://backendapi/internal
This is internal sensitive endpoint
Safeurl
What is it?
A Server Side Request Forgery (SSRF) protection library. Made with 🖤 by Doyensec LLC.
Features:
- Protect against DNS rebinding
- Validation and issuing request done by one library
Fix with safeurl
config := safeurl.GetConfigBuilder().
SetAllowedHosts("imageapi").
Build()
router.GET("/debug", func(context *gin.Context) {
urlFromUser := context.Query("url")
client := safeurl.Client(config)
resp, err := client.Get(urlFromUser)
- in lines 1-3 we defined configuration that will allow only connect to
imageapi
- in line 4 we defined
/debug
endpoint - in line 5 we got input from user
- in line 6 we created http client based on configuration
- in line 7 we issue request proxing it via safeurl
Try with exploit 3
Let’s take our existing explot and try it out with new code featured with safeurl:
$ curl -s \
http://publicapi/debug\?url\=
http://imageapi/redirect\?target\=
http://backendapi/internal
Get "http://imageapi/redirect?target=http://backendapi/
internal": dial tcp 10.96.45.24:80: ip: 10.96.45.24 not
found in allowlist
Great! We successfully mitigated SSRF+Open Redirect chain 😃
Exploit 3 step by step
Let’s dive deeper into this chain of vulnerabilities:
# publicapi
-> /debug request: user-agent=curl/7.86.0
400 | GET "/debug?url=http://imageapi/redirect?target=
http://backendapi/internal"
ip: 10.96.45.24 not found in allowlist
# imageapi
-> /redirect request: user-agent=Go-http-client/1.1
301 | GET "/redirect?target=http://backendapi/internal"
- first Public API is called on
/debug
endpoint - it will validate hostname (using safeurl), which is
imageapi
- OK! - than it will call
imageapi
on/redirect
endpoint - Image API will return
301
redirect tobackendapi
location - Public API will not follow redirect, because safeurl validated this redirect and it’s not in allowlist (imageapi)
Fix with redirect turn off
We don’t need to use safeurl to mitigate those flaws. We can also turn off redirects in http
client:
http.DefaultClient.CheckRedirect =
func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
Try with exploit 3
Let’s try it with turned off redirects:
$ curl -s \
http://publicapi/debug\?url\=
http://imageapi/redirect\?target\=
http://backendapi/internal
<a href="http://backendapi/internal">Moved Permanently</a>.
Bit different response, but similar result. Which is better? Safeurl can handle redirects that targets allowlist, but it’s additional library to your project. You need to decide.
Safeurl in details
Let’s dive deep into safeurl to see how it was implemented:
func buildHttpClient(wc *WrappedClient) *http.Client {
client := &http.Client{
...
CheckRedirect: wc.config.CheckRedirect,
Transport: &http.Transport{
TLSClientConfig: wc.tlsConfig,
DialContext: (&net.Dialer{
Resolver: wc.resolver,
Control: buildRunFunc(wc),
}).DialContext,
Above function is used to build http
client. Safeurl is wrapping that client to control network communication:
- we can see that redirects are controled with config function like this:
wc.config.CheckRedirect
- most important is
DialContext
which hasResolver: wc.resolver
andControl: buildRunFunc(wc)
- this way we can inspect and control every network call of
http
client
- this way we can inspect and control every network call of
Remarkable how easy it’s in go
to wire into low level network call for http
. Well done!
Other controls for SSRF mitigation
What else we can do to be protected? SSRF mitigation is not only about code but also about architecture:
- all outbound network traffic should go through egress proxy
- internal Kubernetes network should be segmented using Network Policies
- microservice endpoints should require authentication
Real life SSRF attacks
With this vulnerability attacker can:
- steal cloud metadata credentials (169.254.169.254)
- steal sensitive tokens (attached by service to http call)
- call internal network sensitive resources
- escalate to RCE via 3rd party apps, e.g. redis, Confluence
Typical places where SSRF can occur:
- webhooks
- file imports from urls
- PDF generators
Summary
- if possible not consume full URLs from users (😅 webhooks)
- apply application level protection - doyensec/safeurl or custom implementation
- apply zero trust architecture protection - network segmentation and authentication
Code
You can try yourself with code that is ready to run on your local: https://github.com/xvnpw/ssrf-in-go
If you have any comments or feedback, you are welcome to write to me on Twitter.