@@ -3,8 +3,11 @@ package response
33import (
44 "encoding/json"
55 "encoding/xml"
6+ "errors"
67 htmltemplate "html/template"
78 "net/http"
9+ "net/url"
10+ "strings"
811 "text/template"
912)
1013
@@ -159,6 +162,9 @@ func XML(w http.ResponseWriter, status int, data any) error {
159162// This is a generic redirect function that allows you to specify any redirect status code.
160163// The Location header is set to the provided URL and the appropriate status code is returned.
161164//
165+ // WARNING: This function does not validate the redirect URL. If the URL comes
166+ // from user input, use [SafeRedirect] instead to prevent open redirect attacks.
167+ //
162168// Common redirect status codes:
163169// - 301: Moved Permanently
164170// - 302: Found (temporary redirect)
@@ -175,3 +181,52 @@ func Redirect(w http.ResponseWriter, status int, url string) error {
175181
176182 return Status (w , status )
177183}
184+
185+ // ErrUnsafeRedirect is returned by [SafeRedirect] when the target
186+ // URL is not a safe relative path. This prevents open redirect
187+ // attacks where an attacker tricks users into visiting a malicious
188+ // external site via your application's redirect endpoint.
189+ var ErrUnsafeRedirect = errors .New ("unsafe redirect URL: must be a relative path" )
190+
191+ // SafeRedirect sends an HTTP redirect response only if the target
192+ // URL is a safe relative path (starts with "/" and does not contain
193+ // a scheme, host, or protocol-relative prefix "//"). This prevents
194+ // open redirect vulnerabilities when the redirect target comes from
195+ // user input such as query parameters or form fields.
196+ //
197+ // Returns [ErrUnsafeRedirect] if the URL fails validation.
198+ //
199+ // Parameters:
200+ // - w: The HTTP response writer
201+ // - status: The HTTP redirect status code to set
202+ // - rawURL: The URL to redirect the user to (must be a relative path)
203+ func SafeRedirect (w http.ResponseWriter , status int , rawURL string ) error {
204+ if ! isRelativePath (rawURL ) {
205+ return ErrUnsafeRedirect
206+ }
207+
208+ return Redirect (w , status , rawURL )
209+ }
210+
211+ // isRelativePath validates that a URL is a safe relative path
212+ // and not an absolute URL, protocol-relative URL, javascript:
213+ // URI, or data: URI that could be used in an open redirect attack.
214+ func isRelativePath (rawURL string ) bool {
215+ if ! strings .HasPrefix (rawURL , "/" ) {
216+ return false
217+ }
218+
219+ // Reject protocol-relative URLs like "//evil.com"
220+ if strings .HasPrefix (rawURL , "//" ) {
221+ return false
222+ }
223+
224+ parsed , err := url .Parse (rawURL )
225+
226+ if err != nil {
227+ return false
228+ }
229+
230+ // Reject if a scheme or host is present.
231+ return parsed .Scheme == "" && parsed .Host == ""
232+ }
0 commit comments