Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ require (
go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/trace v1.44.0
go.uber.org/goleak v1.3.0
golang.org/x/net v0.56.0
golang.org/x/term v0.44.0
golang.org/x/text v0.39.0
google.golang.org/genproto/googleapis/api v0.0.0-20260420184626-e10c466a9529
Expand Down Expand Up @@ -176,7 +177,6 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/time v0.15.0 // indirect
Expand Down
60 changes: 60 additions & 0 deletions network/urlguard/admit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package urlguard

import (
"fmt"
"slices"
"strings"
)

// Ceiling is the manifest-declared origin ceiling. A concrete origin is
// admitted only if it stays inside every dimension of the ceiling. Host
// patterns are bounded: either an exact host or a single "*." wildcard that
// matches proper subdomains only.
type Ceiling struct {
Schemes []string
HostPatterns []string
Ports []uint32
AllowedClasses []NetworkClass
}

// Admit normalizes raw and confirms it stays within the ceiling. It returns
// the normalized origin on success. Admission is purely structural: it does
// not resolve DNS. The caller pins a peer with Resolve and dials with Client.
func (c Ceiling) Admit(raw string) (Origin, error) {
origin, err := NormalizeOrigin(raw)
if err != nil {
return Origin{}, err
}
return origin, c.AdmitOrigin(origin)
}

// AdmitOrigin confirms an already-normalized origin stays within the ceiling.
func (c Ceiling) AdmitOrigin(origin Origin) error {
if !slices.Contains(c.Schemes, origin.Scheme) {
return fmt.Errorf("origin scheme %q is outside the ceiling", origin.Scheme)
}
if !slices.Contains(c.Ports, origin.Port) {
return fmt.Errorf("origin port %d is outside the ceiling", origin.Port)
}
if !hostMatchesAny(origin.Host, c.HostPatterns) {
return fmt.Errorf("origin host %q is outside the ceiling", origin.Host)
}
return nil
}

// hostMatchesAny reports whether host is an exact match for a pattern or a
// proper subdomain of a "*." pattern. The apex of a wildcard pattern is not
// matched by the wildcard itself.
func hostMatchesAny(host string, patterns []string) bool {
for _, pattern := range patterns {
if host == pattern {
return true
}
if suffix, ok := strings.CutPrefix(pattern, "*."); ok {
if strings.HasSuffix(host, "."+suffix) {
return true
}
}
}
return false
}
54 changes: 54 additions & 0 deletions network/urlguard/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package urlguard_test

import (
"testing"

"github.com/codefly-dev/core/network/urlguard"
)

// FuzzNormalizeOrigin proves URL normalization never panics and that an
// admitted origin is always in canonical form (round-trips through parse).
func FuzzNormalizeOrigin(f *testing.F) {
for _, seed := range []string{
"https://api.example.com",
"https://API.Example.com.:8443",
"http://user:pass@host/path?q#f",
"//scheme-relative",
"https://[::1]",
"ftp://host",
"https://xn--pple-43d.com",
} {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, raw string) {
origin, err := urlguard.NormalizeOrigin(raw)
if err != nil {
return
}
// A normalized origin must re-normalize to itself.
again, err := urlguard.NormalizeOrigin(origin.String())
if err != nil {
t.Fatalf("normalized origin %q did not re-normalize: %v", origin.String(), err)
}
if again != origin {
t.Fatalf("normalization is not idempotent: %v -> %v", origin, again)
}
})
}

// FuzzSafePath proves path validation never panics and never admits traversal
// or a query/fragment.
func FuzzSafePath(f *testing.F) {
for _, seed := range []string{"/", "/v1/x", "/a/../b", "/a%2e%2e/b", "/a?x=1", "//b"} {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, path string) {
safe, err := urlguard.SafePath(path)
if err != nil {
return
}
if safe == "" || safe[0] != '/' {
t.Fatalf("safe path %q is not absolute", safe)
}
})
}
Loading
Loading