Skip to content

Commit 0914541

Browse files
authored
Merge pull request #1579 from hodyhq/feat/allow-private-webhook-targets
2 parents eabad3c + 775ea4c commit 0914541

4 files changed

Lines changed: 54 additions & 1 deletion

File tree

.example.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ GO_ENV=development
33
DATABASE_URL=postgres://fider:fider_pw@localhost:5555/fider?sslmode=disable
44
JWT_SECRET=hsjl]W;&ZcHxT&FK;s%bgIQF:#ch=~#Al4:5]N;7V<qPZ3e9lT4'%;go;LIkc%k
55
# ALLOW_ALLOWED_SCHEMES=false
6+
# Relaxes the SSRF guard so webhooks and OAuth token/profile URLs may target
7+
# private/internal network addresses (LAN, loopback, link-local). Self-hosted only.
8+
# ALLOW_PRIVATE_NETWORK_TARGETS=true
69

710
LOG_LEVEL=DEBUG
811
LOG_CONSOLE=true

app/pkg/env/env.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type config struct {
5656
JWTSecret string `env:"JWT_SECRET,required"`
5757
PostCreationWithTagsEnabled bool `env:"POST_CREATION_WITH_TAGS_ENABLED,default=false"`
5858
AllowAllowedSchemes bool `env:"ALLOW_ALLOWED_SCHEMES,default=true"`
59+
AllowPrivateNetworkTargets bool `env:"ALLOW_PRIVATE_NETWORK_TARGETS,default=false"`
5960
Stripe struct {
6061
SecretKey string `env:"STRIPE_SECRET_KEY"`
6162
WebhookSecret string `env:"STRIPE_WEBHOOK_SECRET"`

app/pkg/validate/general.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ func URL(ctx context.Context, rawurl string) []string {
5656
return []string{}
5757
}
5858

59-
// WebhookURL validates that a URL is well-formed and does not target private/internal networks
59+
// WebhookURL validates that a URL is well-formed and, by default, does not target
60+
// private/internal networks. This is a shared SSRF guard: besides webhooks it also
61+
// validates OAuth token/profile URLs. Self-hosted instances that need to reach
62+
// internal services (e.g. an integration server on a LAN) can relax the network
63+
// check by setting ALLOW_PRIVATE_NETWORK_TARGETS=true; format validation (valid URL,
64+
// scheme http/https) still applies in that mode.
6065
func WebhookURL(rawurl string) []string {
6166
u, err := url.Parse(rawurl)
6267
if err != nil || u.Host == "" {
@@ -68,6 +73,10 @@ func WebhookURL(rawurl string) []string {
6873
return []string{"Only http and https URLs are allowed."}
6974
}
7075

76+
if env.Config.AllowPrivateNetworkTargets {
77+
return []string{}
78+
}
79+
7180
hostname := u.Hostname()
7281

7382
if strings.EqualFold(hostname, "localhost") {

app/pkg/validate/general_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/getfider/fider/app/models/query"
88
. "github.com/getfider/fider/app/pkg/assert"
99
"github.com/getfider/fider/app/pkg/bus"
10+
"github.com/getfider/fider/app/pkg/env"
1011
"github.com/getfider/fider/app/pkg/rand"
1112
"github.com/getfider/fider/app/pkg/validate"
1213
)
@@ -114,6 +115,45 @@ func TestWebhookURL_AllowedAddresses(t *testing.T) {
114115
}
115116
}
116117

118+
func TestWebhookURL_PrivateIPsAllowedWhenOptedIn(t *testing.T) {
119+
RegisterT(t)
120+
121+
original := env.Config.AllowPrivateNetworkTargets
122+
env.Config.AllowPrivateNetworkTargets = true
123+
t.Cleanup(func() { env.Config.AllowPrivateNetworkTargets = original })
124+
125+
for _, rawurl := range []string{
126+
"http://localhost/hook",
127+
"http://127.0.0.1/hook",
128+
"http://10.0.0.1/hook",
129+
"http://172.16.0.1/hook",
130+
"http://192.168.1.1:8080/hook",
131+
"http://[::1]/hook",
132+
"http://internal.lan/hook",
133+
} {
134+
messages := validate.WebhookURL(rawurl)
135+
Expect(messages).HasLen(0)
136+
}
137+
}
138+
139+
func TestWebhookURL_OptInDoesNotBypassFormatValidation(t *testing.T) {
140+
RegisterT(t)
141+
142+
original := env.Config.AllowPrivateNetworkTargets
143+
env.Config.AllowPrivateNetworkTargets = true
144+
t.Cleanup(func() { env.Config.AllowPrivateNetworkTargets = original })
145+
146+
for _, rawurl := range []string{
147+
"ftp://example.com/hook",
148+
"file:///etc/passwd",
149+
"not a url at all",
150+
"",
151+
} {
152+
messages := validate.WebhookURL(rawurl)
153+
Expect(len(messages) > 0).IsTrue()
154+
}
155+
}
156+
117157
func TestInvalidCNAME(t *testing.T) {
118158
RegisterT(t)
119159

0 commit comments

Comments
 (0)