Skip to content

Commit e7948be

Browse files
committed
do not force hostname validation for test keys
1 parent 0d728f1 commit e7948be

2 files changed

Lines changed: 91 additions & 1 deletion

File tree

main.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,23 @@ const (
3838
DefaultHealthCheckFailureThreshold = 0 // Number of consecutive health check failures before opening circuit
3939
goodBotLookupTimeout = 2 * time.Second
4040
maxCaptchaChallengeAge = 5 * time.Minute
41+
turnstileTestHostname = "example.com"
4142
)
4243

44+
var turnstileTestSiteKeys = map[string]struct{}{
45+
"1x00000000000000000000AA": {},
46+
"2x00000000000000000000AB": {},
47+
"1x00000000000000000000BB": {},
48+
"2x00000000000000000000BB": {},
49+
"3x00000000000000000000FF": {},
50+
}
51+
52+
var turnstileTestSecretKeys = map[string]struct{}{
53+
"1x0000000000000000000000000000000AA": {},
54+
"2x0000000000000000000000000000000AA": {},
55+
"3x0000000000000000000000000000000AA": {},
56+
}
57+
4358
type circuitState int
4459

4560
const (
@@ -696,7 +711,8 @@ func (bc *CaptchaProtect) verifyChallengePage(rw http.ResponseWriter, req *http.
696711
success = captchaResponse.Success
697712
if success && activeConfig.key == "cf-turnstile" {
698713
expectedHostname := captchaValidationHostname(req)
699-
if captchaResponse.Hostname != expectedHostname {
714+
skipHostnameValidation := bc.usesTurnstileTestKeys() && captchaResponse.Hostname == turnstileTestHostname
715+
if !skipHostnameValidation && captchaResponse.Hostname != expectedHostname {
700716
bc.log.Warn("captcha hostname mismatch", "hostname", captchaResponse.Hostname, "expectedHostname", expectedHostname)
701717
success = false
702718
} else {
@@ -743,6 +759,12 @@ func captchaValidationHostname(req *http.Request) string {
743759
return host
744760
}
745761

762+
func (bc *CaptchaProtect) usesTurnstileTestKeys() bool {
763+
_, hasTestSiteKey := turnstileTestSiteKeys[bc.config.SiteKey]
764+
_, hasTestSecretKey := turnstileTestSecretKeys[bc.config.SecretKey]
765+
return hasTestSiteKey && hasTestSecretKey
766+
}
767+
746768
func randomUUID() (string, error) {
747769
var b [16]byte
748770
if _, err := crand.Read(b[:]); err != nil {

main_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,74 @@ func TestVerifyChallengePageRejectsInvalidSiteverifyMetadata(t *testing.T) {
11611161
}
11621162
}
11631163

1164+
func TestVerifyChallengePageAllowsTurnstileTestKeysExampleHostname(t *testing.T) {
1165+
validChallengeTS := time.Now().Format(time.RFC3339Nano)
1166+
mockResponse := fmt.Sprintf(`{"success":true,"hostname":"example.com","challenge_ts":%q}`, validChallengeTS)
1167+
1168+
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1169+
w.Header().Set("Content-Type", "application/json")
1170+
_, _ = w.Write([]byte(mockResponse))
1171+
}))
1172+
defer mockServer.Close()
1173+
1174+
config := CreateConfig()
1175+
config.SiteKey = "1x00000000000000000000AA"
1176+
config.SecretKey = "1x0000000000000000000000000000000AA"
1177+
config.ProtectRoutes = []string{"/"}
1178+
config.CaptchaProvider = "turnstile"
1179+
1180+
bc, _ := NewCaptchaProtect(context.Background(), nil, config, "test")
1181+
bc.captchaConfig.validate = mockServer.URL
1182+
1183+
req := httptest.NewRequest(http.MethodPost, "http://localhost/challenge", nil)
1184+
req.Form = make(map[string][]string)
1185+
req.Form.Set("cf-turnstile-response", "valid-token")
1186+
1187+
rr := httptest.NewRecorder()
1188+
status := bc.verifyChallengePage(rr, req, "1.2.3.4")
1189+
1190+
if status != http.StatusFound {
1191+
t.Fatalf("expected status %d, got %d", http.StatusFound, status)
1192+
}
1193+
if _, found := bc.verifiedCache.Get("1.2.3.4"); !found {
1194+
t.Fatal("expected turnstile test key response to set verified cache")
1195+
}
1196+
}
1197+
1198+
func TestVerifyChallengePageRejectsExampleHostnameWithoutTurnstileTestKeys(t *testing.T) {
1199+
validChallengeTS := time.Now().Format(time.RFC3339Nano)
1200+
mockResponse := fmt.Sprintf(`{"success":true,"hostname":"example.com","challenge_ts":%q}`, validChallengeTS)
1201+
1202+
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1203+
w.Header().Set("Content-Type", "application/json")
1204+
_, _ = w.Write([]byte(mockResponse))
1205+
}))
1206+
defer mockServer.Close()
1207+
1208+
config := CreateConfig()
1209+
config.SiteKey = "test"
1210+
config.SecretKey = "test"
1211+
config.ProtectRoutes = []string{"/"}
1212+
config.CaptchaProvider = "turnstile"
1213+
1214+
bc, _ := NewCaptchaProtect(context.Background(), nil, config, "test")
1215+
bc.captchaConfig.validate = mockServer.URL
1216+
1217+
req := httptest.NewRequest(http.MethodPost, "http://localhost/challenge", nil)
1218+
req.Form = make(map[string][]string)
1219+
req.Form.Set("cf-turnstile-response", "valid-token")
1220+
1221+
rr := httptest.NewRecorder()
1222+
status := bc.verifyChallengePage(rr, req, "1.2.3.4")
1223+
1224+
if status != http.StatusForbidden {
1225+
t.Fatalf("expected status %d, got %d", http.StatusForbidden, status)
1226+
}
1227+
if _, found := bc.verifiedCache.Get("1.2.3.4"); found {
1228+
t.Fatal("did not expect non-test key response to set verified cache")
1229+
}
1230+
}
1231+
11641232
func TestVerifyChallengePageAllowsNonTurnstileWithoutMetadata(t *testing.T) {
11651233
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
11661234
w.Header().Set("Content-Type", "application/json")

0 commit comments

Comments
 (0)