44 "context"
55 "encoding/json"
66 "fmt"
7+ htemplate "html/template"
78 "log/slog"
89 "math/rand"
910 "net"
@@ -15,7 +16,6 @@ import (
1516 "slices"
1617 "strings"
1718 "sync"
18- "text/template"
1919 "time"
2020
2121 "github.com/libops/captcha-protect/internal/helper"
@@ -93,7 +93,7 @@ type CaptchaProtect struct {
9393 googlebotIPs * helper.GooglebotIPs
9494 captchaConfig CaptchaConfig
9595 exemptIps []* net.IPNet
96- tmpl * template .Template
96+ tmpl * htemplate .Template
9797 ipv4Mask net.IPMask
9898 ipv6Mask net.IPMask
9999 protectRoutesRegex []* regexp.Regexp
@@ -116,6 +116,14 @@ type captchaResponse struct {
116116 Success bool `json:"success"`
117117}
118118
119+ type challengeData struct {
120+ SiteKey string
121+ FrontendJS string
122+ FrontendKey string
123+ ChallengeURL string
124+ Destination string
125+ }
126+
119127func CreateConfig () * Config {
120128 return & Config {
121129 RateLimit : 20 ,
@@ -217,18 +225,18 @@ func NewCaptchaProtect(ctx context.Context, next http.Handler, config *Config, n
217225 }
218226 config .ParseHttpMethods (log )
219227
220- var tmpl * template .Template
228+ var tmpl * htemplate .Template
221229 if _ , err := os .Stat (config .ChallengeTmpl ); os .IsNotExist (err ) {
222230 log .Warn ("Unable to find template file. Using default template" , "challengeTmpl" , config .ChallengeTmpl )
223231 ts := helper .GetDefaultTmpl ()
224- tmpl , err = template .New ("challenge" ).Parse (ts )
232+ tmpl , err = htemplate .New ("challenge" ).Parse (ts )
225233 if err != nil {
226234 return nil , fmt .Errorf ("unable to parse challenge template: %v" , err )
227235 }
228236 } else if err != nil {
229237 return nil , fmt .Errorf ("error checking for template file %s: %v" , config .ChallengeTmpl , err )
230238 } else {
231- tmpl , err = template .ParseFiles (config .ChallengeTmpl )
239+ tmpl , err = htemplate .ParseFiles (config .ChallengeTmpl )
232240 if err != nil {
233241 return nil , fmt .Errorf ("unable to parse challenge template file %s: %v" , config .ChallengeTmpl , err )
234242 }
@@ -590,12 +598,12 @@ func (bc *CaptchaProtect) servePojJS(rw http.ResponseWriter) {
590598func (bc * CaptchaProtect ) serveChallengePage (rw http.ResponseWriter , destination string ) {
591599 activeConfig := bc .getActiveCaptchaConfig ()
592600
593- d := map [ string ] string {
594- " SiteKey" : bc .config .SiteKey ,
595- " FrontendJS" : activeConfig .js ,
596- " FrontendKey" : activeConfig .key ,
597- " ChallengeURL" : bc .config .ChallengeURL ,
598- " Destination" : destination ,
601+ d := challengeData {
602+ SiteKey : bc .config .SiteKey ,
603+ FrontendJS : activeConfig .js ,
604+ FrontendKey : activeConfig .key ,
605+ ChallengeURL : bc .config .ChallengeURL ,
606+ Destination : destination ,
599607 }
600608
601609 rw .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
@@ -662,16 +670,8 @@ func (bc *CaptchaProtect) verifyChallengePage(rw http.ResponseWriter, req *http.
662670 if success {
663671 bc .verifiedCache .Set (ip , true , exp )
664672
665- destination := req .FormValue ("destination" )
666- if destination == "" {
667- destination = "%2F"
668- }
669- u , err := url .QueryUnescape (destination )
670- if err != nil {
671- bc .log .Error ("unable to unescape destination" , "destination" , destination , "err" , err )
672- u = "/"
673- }
674- http .Redirect (rw , req , u , http .StatusFound )
673+ destination := normalizeDestination (req .FormValue ("destination" ))
674+ http .Redirect (rw , req , destination , http .StatusFound )
675675 return http .StatusFound
676676 }
677677
@@ -680,6 +680,42 @@ func (bc *CaptchaProtect) verifyChallengePage(rw http.ResponseWriter, req *http.
680680 return http .StatusForbidden
681681}
682682
683+ func normalizeDestination (destination string ) string {
684+ if destination == "" {
685+ return "/"
686+ }
687+
688+ unescaped , err := url .QueryUnescape (destination )
689+ if err == nil && unescaped != destination {
690+ if sanitized := sanitizeDestination (unescaped ); sanitized != "/" || unescaped == "/" {
691+ return sanitized
692+ }
693+ }
694+
695+ return sanitizeDestination (destination )
696+ }
697+
698+ func sanitizeDestination (destination string ) string {
699+ if destination == "" {
700+ return "/"
701+ }
702+
703+ u , err := url .Parse (destination )
704+ if err != nil {
705+ return "/"
706+ }
707+
708+ if u .IsAbs () || u .Host != "" {
709+ return "/"
710+ }
711+
712+ if ! strings .HasPrefix (u .Path , "/" ) {
713+ return "/"
714+ }
715+
716+ return u .RequestURI ()
717+ }
718+
683719func (bc * CaptchaProtect ) serveStatsPage (rw http.ResponseWriter , ip string ) {
684720 // only allow excluded IPs from viewing
685721 if ! helper .IsIpExcluded (ip , bc .exemptIps ) {
0 commit comments