Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
16 changes: 15 additions & 1 deletion policy/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,19 @@ func (p *Policy) Print(ctx print.Context, msg string) error {
return nil
}

func normalizeHTTPHost(u *url.URL) string {
host := u.Host
port := strings.TrimLeft(u.Port(), "0")
if u.Scheme == "http" && port == "80" || u.Scheme == "https" && port == "443" {
host = strings.TrimSuffix(host, ":"+u.Port())
Comment thread
crazy-max marked this conversation as resolved.
Outdated
}
// Preserve IPv6 zone identifiers, which may be case-sensitive interface names.
if i := strings.IndexByte(host, '%'); strings.HasPrefix(host, "[") && i >= 0 {
return strings.ToLower(host[:i]) + host[i:]
}
Comment thread
crazy-max marked this conversation as resolved.
Outdated
return strings.ToLower(host)
}

func sourceToInput(ctx context.Context, getVerifier PolicyVerifierProvider, src *gwpb.ResolveSourceMetaResponse, platform *ocispecs.Platform, logf func(logrus.Level, string)) (Input, []string, error) {
var inp Input
var unknowns []string
Expand All @@ -544,6 +557,7 @@ func sourceToInput(ctx context.Context, getVerifier PolicyVerifierProvider, src
if !ok {
return inp, nil, errors.Errorf("invalid source identifier: %s", src.Source.Identifier)
}
scheme = strings.ToLower(scheme)

switch scheme {
case "http", "https":
Expand All @@ -554,7 +568,7 @@ func sourceToInput(ctx context.Context, getVerifier PolicyVerifierProvider, src
inp.HTTP = &HTTP{
URL: src.Source.Identifier,
Schema: scheme,
Host: u.Host,
Host: normalizeHTTPHost(u),
Path: u.Path,
Query: u.Query(),
}
Expand Down
275 changes: 275 additions & 0 deletions policy/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import (
"encoding/json"
"errors"
"fmt"
"net/url"
"testing"
"time"

slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
"github.com/moby/buildkit/solver/pb"
policyaction "github.com/moby/buildkit/sourcepolicy/pb"
"github.com/moby/buildkit/sourcepolicy/policysession"
policyverifier "github.com/moby/policy-helpers"
policyimage "github.com/moby/policy-helpers/image"
policytypes "github.com/moby/policy-helpers/types"
Expand All @@ -23,6 +26,44 @@ import (
"github.com/stretchr/testify/require"
)

func TestNormalizeHTTPHost(t *testing.T) {
tests := []struct {
scheme, host, want string
}{
{"https", "example.com", "example.com"},
{"http", "Example.com", "example.com"},
{"https", "EXAMPLE.COM:000443", "example.com"},
{"https", "eXaMpLe.CoM:8443", "example.com:8443"},
{"https", "example.com:443", "example.com"},
{"http", "example.com:80", "example.com"},
Comment thread
crazy-max marked this conversation as resolved.
{"https", ":443", ""},
{"http", ":80", ""},
{"https", "example.com:000443", "example.com"},
{"http", "example.com:00080", "example.com"},
{"https", "example.com:00080", "example.com:00080"},
{"http", "example.com:000443", "example.com:000443"},
{"https", "example.com:008443", "example.com:008443"},
{"http", "example.com:000", "example.com:000"},
{"https", "example.com:80", "example.com:80"},
{"http", "example.com:443", "example.com:443"},
{"https", "example.com:8443", "example.com:8443"},
{"http", "example.com:8080", "example.com:8080"},
Comment thread
crazy-max marked this conversation as resolved.
{"https", "[2001:db8::1]", "[2001:db8::1]"},
Comment thread
crazy-max marked this conversation as resolved.
{"https", "[2001:DB8::ABCD]:443", "[2001:db8::abcd]"},
{"https", "[FE80::ABCD%Eth0]:000443", "[fe80::abcd%Eth0]"},
{"https", "[FE80::ABCD%Eth0]:8443", "[fe80::abcd%Eth0]:8443"},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my other comment; this is an invalid address, and would be rejected.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test constructs url.URL{Host: ...} directly, so the decoded %Eth0 is intentional. The https-ipv6-zone-case-preserved source-conversion test separately parses a URL containing %25Eth0 and verifies the decoded host.

{"https", "[2001:db8::1]:443", "[2001:db8::1]"},
{"https", "[2001:db8::1]:000443", "[2001:db8::1]"},
{"http", "[2001:db8::1]:00080", "[2001:db8::1]"},
{"https", "[2001:db8::1]:8443", "[2001:db8::1]:8443"},
}
for _, tt := range tests {
t.Run(tt.scheme+" "+tt.host, func(t *testing.T) {
require.Equal(t, tt.want, normalizeHTTPHost(&url.URL{Scheme: tt.scheme, Host: tt.host}))
})
}
}

func TestSourceToInputSingleSource(t *testing.T) {
tm := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)

Expand All @@ -48,6 +89,68 @@ func TestSourceToInputSingleSource(t *testing.T) {
},
expErrMsg: "invalid source identifier: not-a-source",
},
{
name: "http-ipv6-without-brackets",
src: &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{Identifier: "http://::1:443"},
},
expErrMsg: "failed to parse http source url",
},
{
name: "https-ipv6-without-brackets",
src: &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{Identifier: "https://::1:443"},
},
expErrMsg: "failed to parse http source url",
},
{
name: "http-mixed-case-scheme-and-host",
src: &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{Identifier: "hTtP://User:PaSs@eXaMpLe.CoM:00080/Case/Path?Key=VaLuE#Frag"},
},
expInput: Input{
HTTP: &HTTP{
URL: "hTtP://User:PaSs@eXaMpLe.CoM:00080/Case/Path?Key=VaLuE#Frag",
Schema: "http",
Host: "example.com",
Path: "/Case/Path",
Query: map[string][]string{"Key": {"VaLuE"}},
},
},
expUnk: []string{"input.http.checksum"},
},
{
name: "https-mixed-case-host-with-non-default-port",
src: &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{Identifier: "HTTPS://EXAMPLE.COM:8443/Case?Key=Value"},
},
expInput: Input{
HTTP: &HTTP{
URL: "HTTPS://EXAMPLE.COM:8443/Case?Key=Value",
Schema: "https",
Host: "example.com:8443",
Path: "/Case",
Query: map[string][]string{"Key": {"Value"}},
},
},
expUnk: []string{"input.http.checksum"},
},
{
name: "https-ipv6-zone-case-preserved",
src: &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{Identifier: "HTTPS://[FE80::ABCD%25Eth0]:000443/Case"},
},
expInput: Input{
HTTP: &HTTP{
URL: "HTTPS://[FE80::ABCD%25Eth0]:000443/Case",
Schema: "https",
Host: "[fe80::abcd%Eth0]",
Path: "/Case",
Query: map[string][]string{},
},
},
expUnk: []string{"input.http.checksum"},
},
{
name: "http-source-with-checksum-and-auth",
src: &gwpb.ResolveSourceMetaResponse{
Expand Down Expand Up @@ -133,6 +236,120 @@ func TestSourceToInputSingleSource(t *testing.T) {
},
},
},
{
name: "https-default-port-stripped-from-host",
src: &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{
Identifier: "https://example.com:443/foo.tar.gz",
},
HTTP: &gwpb.ResolveSourceHTTPResponse{
Checksum: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
},
expInput: Input{
HTTP: &HTTP{
URL: "https://example.com:443/foo.tar.gz",
Schema: "https",
Host: "example.com",
Path: "/foo.tar.gz",
Query: map[string][]string{},
Checksum: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
},
},
{
name: "http-default-port-stripped-from-host",
src: &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{
Identifier: "http://example.com:80/foo.tar.gz",
},
HTTP: &gwpb.ResolveSourceHTTPResponse{
Checksum: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
},
},
expInput: Input{
HTTP: &HTTP{
URL: "http://example.com:80/foo.tar.gz",
Schema: "http",
Host: "example.com",
Path: "/foo.tar.gz",
Query: map[string][]string{},
Checksum: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
},
},
},
{
name: "http-empty-host-with-default-port",
src: &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{Identifier: "http://:80"},
},
expInput: Input{
HTTP: &HTTP{
URL: "http://:80",
Schema: "http",
Host: "",
Query: map[string][]string{},
},
},
expUnk: []string{"input.http.checksum"},
},
{
name: "https-empty-host-with-default-port",
src: &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{Identifier: "https://:443"},
},
expInput: Input{
HTTP: &HTTP{
URL: "https://:443",
Schema: "https",
Host: "",
Query: map[string][]string{},
},
},
expUnk: []string{"input.http.checksum"},
},
{
name: "https-non-default-port-kept-on-host",
src: &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{
Identifier: "https://example.com:8443/foo.tar.gz",
},
HTTP: &gwpb.ResolveSourceHTTPResponse{
Checksum: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
},
},
expInput: Input{
HTTP: &HTTP{
URL: "https://example.com:8443/foo.tar.gz",
Schema: "https",
Host: "example.com:8443",
Path: "/foo.tar.gz",
Query: map[string][]string{},
Checksum: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
},
},
},
{
name: "https-ipv6-default-port-stripped-from-host",
src: &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{
Identifier: "https://[2001:db8::1]:443/foo.tar.gz",
},
HTTP: &gwpb.ResolveSourceHTTPResponse{
Checksum: "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
},
},
expInput: Input{
HTTP: &HTTP{
URL: "https://[2001:db8::1]:443/foo.tar.gz",
Schema: "https",
Host: "[2001:db8::1]",
Path: "/foo.tar.gz",
Query: map[string][]string{},
Checksum: "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
},
},
},
{
name: "local-source",
src: &gwpb.ResolveSourceMetaResponse{
Expand Down Expand Up @@ -883,6 +1100,64 @@ func TestSourceToInputSingleSource(t *testing.T) {
}
}

func TestCheckPolicyHTTPHost(t *testing.T) {
for _, tc := range []struct {
url string
allow bool
}{
{"https://example.com/", true},
{"Http://Example.com/", true},
{"hTtP://eXaMpLe.CoM/", true},
{"HTTP://EXAMPLE.COM/", true},
{"HTTPS://EXAMPLE.COM:000443/", true},
{"HTTPS://EXAMPLE.COM:8443/", false},
{"https://example.com:443/", true},
{"http://example.com:80/", true},
{"https://example.com:000443/", true},
{"http://example.com:00080/", true},
{"https://example.com:00080/", false},
{"http://example.com:000443/", false},
{"https://example.com:008443/", false},
{"https://example.com:80/", false},
{"http://example.com:443/", false},
{"https://example.com:8443/", false},
{"https://[2001:db8::1]:443/", true},
{"https://[2001:db8::1]:000443/", true},
{"http://[2001:db8::1]:00080/", true},
{"https://[2001:db8::1]:8443/", false},
} {
t.Run(tc.url, func(t *testing.T) {
p := NewPolicy(Opt{
Files: []File{{
Filename: "policy.rego",
Data: []byte(`
package docker

default allow := false
allow if input.http.host in ["example.com", "[2001:db8::1]"]
decision := {"allow": allow}
`),
}},
})

// Exec proxy requests do not include HTTP checksum metadata.
decision, next, err := p.CheckPolicy(t.Context(), &policysession.CheckPolicyRequest{
Source: &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{Identifier: tc.url},
},
})
require.NoError(t, err)
require.Nil(t, next)
require.NotNil(t, decision)
want := policyaction.PolicyAction_DENY
if tc.allow {
want = policyaction.PolicyAction_ALLOW
}
require.Equal(t, want, decision.Action)
})
}
}

func TestCheckCaps(t *testing.T) {
p := NewPolicy(Opt{
Files: []File{{
Expand Down
Loading