Protecting Your Go HTTP Server From Malicious User Input
Introduction
When handling user input, it’s crucial to implement appropriate safeguards against malicious payloads that can potentially overwhelm your server. In this blog post, we will explore a common vulnerability in an HTTP server’s public endpoint(s) and demonstrate how to mitigate it.
Vulnerable
You have an HTTP server with a login endpoint accepting a JSON body containing email and password that looks something like the following.
package main
import (
"encoding/json"
"net/http"
)
type loginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
func main() {
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
body := &loginRequest{}
err := json.NewDecoder(r.Body).Decode(body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
})
http.ListenAndServe(":1337", nil)
}
Note: I would not recommend configuring your HTTP server in the manner shown above. This is just for the purpose of demonstration and only the handler should be considered. For a more production-ready example of an HTTP server, checkout my earlier post Graceful Shutdown in Go.
Seems fine and dandy until a malicious user crafts a payload containing an endless stream of characters for email.
{"email": "sdfl;kjaslk;dfjasldk;jfakl;sdjflaksjdkfljaskldfj...
...
indicates the endless stream of characters
That HTTP server is going to read the request body input stream until its heart is content, which is never.
Now, if you’re a wise-one, you may have a reverse proxy that sits in front of the server such as Nginx which defaults client_max_body_size
to 1MB
. This will cause requests to be short-circuited after your server happily reads up to that limit. While this is nice, 1MB is an unreasonable limit for an endpoint expecting email and password as input (long passwords are good but relax). Furthermore, you may have endpoints where 1MB may be too small such as image uploads. This may cause you to bump the client_max_body_size
value up and in turn increase your exposure. Furtherfurthermore, deploying your application elsewhere could result in losing that protection making it reasonable to expect the configuration be encoded in your server.
Safe(r)
Lucky for us, the http
standard library package exposes a function to add the exact fine-grained protection on a per-handler level (or as a middleware to apply more broadly) that we desire. http.MaxBytesReader
reads only up to the configured size.
// MaxBytesReader is similar to io.LimitReader but is intended for
// limiting the size of incoming request bodies. In contrast to
// io.LimitReader, MaxBytesReader's result is a ReadCloser, returns a
// non-nil error of type *MaxBytesError for a Read beyond the limit,
// and closes the underlying reader when its Close method is called.
//
// MaxBytesReader prevents clients from accidentally or maliciously
// sending a large request and wasting server resources. If possible,
// it tells the ResponseWriter to close the connection after the limit
// has been reached.
func MaxBytesReader(w ResponseWriter, r io.ReadCloser, n int64) io.ReadCloser {}
We can rewrite the original handler with MaxBytesReader
applied for protection against these malicious payloads as follows.
package main
import (
"encoding/json"
"net/http"
)
type loginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
const maxValidLoginRequestSize = 1024 * 5 // adust this value to fit your use case
func main() {
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxValidLoginRequestSize)
body := &loginRequest{}
err := json.NewDecoder(r.Body).Decode(body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
})
http.ListenAndServe(":1337", nil)
}
In this improved implementation, the r.Body = http.MaxBytesReader(w, r.Body, 1024 * 5)
line wraps the request body with MaxBytesReader
, ensuring that the server reads only up to the configured limit. If the limit is exceeded, decoding the request body will result in a *MaxBytesError
, allowing you to handle it appropriately.
Conclusion
By employing this approach, you can safeguard your Go HTTP server against malicious payloads and mitigate the risk of resource exhaustion or denial of service attacks. Although it’s not a silver-bullet, it provides an easily attainable measure to enhance the availability of your service.
Share your username/password combinations below that this implementation breaks.