Say how to allow a refused private target address (closes #398)
check / check (push) Successful in 5m36s
check / check (push) Successful in 5m36s
Adding or editing an http or slack target whose address is private or reserved was refused with no hint that the refusal is deliberate or that it can be lifted. The refusal now adds that such addresses are refused by default and that the server's ALLOWED_EGRESS_CIDRS setting allows named networks, naming the README section "Allowing egress to your own network". Metadata refusals do not get it. The default blocklist's public addresses move to a list of their own, still checked after the allowlist, and are refused as cloud metadata addresses. The private-and-reserved error is exported as ErrBlockedPrivateOrReservedIP so the handler can tell them apart. Model: opus-5-5
This commit is contained in:
@@ -1577,11 +1577,22 @@ func (h *Handlers) validateTargetURL(
|
||||
"url", delivery.MaskURL(targetURL),
|
||||
"error", err,
|
||||
)
|
||||
http.Error(
|
||||
w,
|
||||
"Invalid target URL: "+err.Error(),
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
|
||||
msg := "Invalid target URL: " + err.Error()
|
||||
|
||||
// Only a private or reserved address's refusal says how
|
||||
// to allow it. Metadata refusals never do: link-local and
|
||||
// the other unconditional metadata addresses cannot be
|
||||
// opened, and the default blocklist's public addresses,
|
||||
// which listing does open, hand out credentials.
|
||||
if errors.Is(err, delivery.ErrBlockedPrivateOrReservedIP) {
|
||||
msg += ". Private and reserved addresses are refused " +
|
||||
"by default; the server's ALLOWED_EGRESS_CIDRS " +
|
||||
"setting allows named networks (see \"Allowing " +
|
||||
"egress to your own network\" in the README)."
|
||||
}
|
||||
|
||||
http.Error(w, msg, http.StatusBadRequest)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sneak.berlin/go/webhooker/internal/database"
|
||||
)
|
||||
|
||||
// privateRefusalHint is the sentence that tells an operator a private
|
||||
// destination is refused on purpose, and how to allow one.
|
||||
const privateRefusalHint = "Private and reserved addresses are " +
|
||||
"refused by default; the server's ALLOWED_EGRESS_CIDRS setting " +
|
||||
"allows named networks (see \"Allowing egress to your own " +
|
||||
"network\" in the README)."
|
||||
|
||||
// TestTargetRefusal_PrivateDestinationSaysHowToAllowIt covers both
|
||||
// target types that take a URL, on add and on edit.
|
||||
func TestTargetRefusal_PrivateDestinationSaysHowToAllowIt(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSourceTest(t)
|
||||
|
||||
targetTypes := []database.TargetType{
|
||||
database.TargetTypeHTTP,
|
||||
database.TargetTypeSlack,
|
||||
}
|
||||
|
||||
for _, targetType := range targetTypes {
|
||||
t.Run(string(targetType), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
webhook := seedWebhookWithRetention(t, env.db, 30)
|
||||
targetsPath := "/source/" + webhook.ID + "/targets"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("name", "private")
|
||||
form.Set("type", string(targetType))
|
||||
form.Set("url", editBlockedURL)
|
||||
|
||||
added := serveTarget(
|
||||
env, http.MethodPost, targetsPath, form,
|
||||
)
|
||||
assert.Equal(t, http.StatusBadRequest, added.Code)
|
||||
assert.Contains(
|
||||
t, added.Body.String(), privateRefusalHint,
|
||||
)
|
||||
|
||||
form.Set("url", editOriginalURL)
|
||||
|
||||
created := serveTarget(
|
||||
env, http.MethodPost, targetsPath, form,
|
||||
)
|
||||
require.Equal(
|
||||
t, http.StatusSeeOther, created.Code,
|
||||
created.Body.String(),
|
||||
)
|
||||
|
||||
targets := targetsForWebhook(t, env.db, webhook.ID)
|
||||
require.Len(t, targets, 1)
|
||||
|
||||
form.Set("url", editBlockedURL)
|
||||
|
||||
edited := submitTargetEdit(
|
||||
env, webhook.ID, targets[0].ID, form,
|
||||
)
|
||||
assert.Equal(t, http.StatusBadRequest, edited.Code)
|
||||
assert.Contains(
|
||||
t, edited.Body.String(), privateRefusalHint,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTargetRefusal_MetadataDestinationDoesNotSayHowToAllowIt: no
|
||||
// setting opens a link-local address, and Azure's WireServer hands out
|
||||
// VM credentials, so neither refusal points at the setting.
|
||||
func TestTargetRefusal_MetadataDestinationDoesNotSayHowToAllowIt(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSourceTest(t)
|
||||
|
||||
metadataURLs := map[string]string{
|
||||
"link-local": "http://169.254.169.254/latest/meta-data/",
|
||||
"wireserver": "http://168.63.129.16/?comp=versions",
|
||||
}
|
||||
|
||||
for name, metadataURL := range metadataURLs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
webhook := seedWebhookWithRetention(t, env.db, 30)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("name", "metadata")
|
||||
form.Set("type", string(database.TargetTypeHTTP))
|
||||
form.Set("url", metadataURL)
|
||||
|
||||
w := serveTarget(
|
||||
env, http.MethodPost,
|
||||
"/source/"+webhook.ID+"/targets", form,
|
||||
)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.NotContains(
|
||||
t, w.Body.String(), privateRefusalHint,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user