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
31 changes: 31 additions & 0 deletions pkg/store/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package utils
import (
"encoding/json"
"io"
"net"
"net/url"
"os"
"strings"

Expand Down Expand Up @@ -38,6 +40,35 @@ func ValidateFunc(function *pms.Function) error {
if function.Name == "" || function.FuncURL == "" {
return errors.New(errors.InvalidRequest, "\"name\" and \"funcURL\" in function definition can not be empty")
}
// Validate the function URL at registration time so that a malicious
// FuncURL pointing at an internal/private address (SSRF) is rejected
// before it is ever stored and evaluated.
return ValidateFunctionURL(function.FuncURL)
}

// ValidateFunctionURL checks that a custom function URL is safe to call: it must
// use http/https and must not resolve to a private, loopback, link-local, or
// unspecified address (SSRF protection). localhost is permitted for local
// development.
func ValidateFunctionURL(urlStr string) error {
parsed, err := url.Parse(urlStr)
if err != nil {
return errors.Wrapf(err, errors.InvalidRequest, "invalid function URL %q", urlStr)
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return errors.Errorf(errors.InvalidRequest, "unsupported URL scheme %q in function URL %q", parsed.Scheme, urlStr)
}
host := parsed.Hostname() // strips brackets from IPv6 and the port
// Allow localhost for local development/testing.
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
return nil
}
if ip := net.ParseIP(host); ip != nil {
if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() ||
ip.IsLinkLocalMulticast() || ip.IsUnspecified() {
return errors.Errorf(errors.InvalidRequest, "function URL %q resolves to a private/internal address, which is not allowed", urlStr)
}
}
return nil
}

Expand Down
54 changes: 54 additions & 0 deletions pkg/store/utils/validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
//Licensed under the Universal Permissive License (UPL) Version 1.0 as shown at http://oss.oracle.com/licenses/upl.

package utils

import (
"testing"

"github.com/teramoby/speedle-plus/api/pms"
)

func TestValidateFunctionURL(t *testing.T) {
cases := []struct {
url string
wantErr bool
}{
// Allowed
{"https://example.com/func", false},
{"http://example.com:8080/func", false},
{"http://localhost:9000/func", false},
{"http://127.0.0.1/func", false},
{"http://[::1]/func", false},
// Rejected: private / internal / metadata addresses (SSRF)
{"http://169.254.169.254/latest/meta-data/", true}, // AWS metadata (link-local)
{"http://10.0.0.5/func", true}, // private
{"http://192.168.1.1/func", true}, // private
{"http://172.16.0.1/func", true}, // private
{"http://[fe80::1]/func", true}, // IPv6 link-local
{"http://0.0.0.0/func", true}, // unspecified
// Rejected: bad scheme / unparseable
{"ftp://example.com/func", true},
{"file:///etc/passwd", true},
{"://nonsense", true},
}
for _, c := range cases {
err := ValidateFunctionURL(c.url)
if (err != nil) != c.wantErr {
t.Errorf("ValidateFunctionURL(%q) error = %v, wantErr = %v", c.url, err, c.wantErr)
}
}
}

func TestValidateFuncRejectsSSRF(t *testing.T) {
// A function registered with an internal metadata URL must be rejected.
fn := &pms.Function{Name: "evil", FuncURL: "http://169.254.169.254/latest/meta-data/"}
if err := ValidateFunc(fn); err == nil {
t.Errorf("expected ValidateFunc to reject SSRF metadata URL, got nil")
}
// A legitimate function must pass.
ok := &pms.Function{Name: "good", FuncURL: "https://func.example.com/run"}
if err := ValidateFunc(ok); err != nil {
t.Errorf("expected ValidateFunc to accept public URL, got %v", err)
}
}
Loading