Pass all input to backend through a input sanitizer.

This commit is contained in:
Russell Jones 2018-05-11 00:36:09 +00:00
parent cc8ae99192
commit 09b3c7f786
13 changed files with 370 additions and 136 deletions

View file

@ -37,7 +37,6 @@ import (
"github.com/gravitational/teleport/lib/utils"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/oidc"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
. "gopkg.in/check.v1"
@ -237,7 +236,7 @@ func (s *AuthSuite) TestTokensCRUD(c *C) {
c.Assert(err, IsNil)
// generate predefined token
customToken := "custom token"
customToken := "custom-token"
tok, err = s.a.GenerateToken(GenerateTokenRequest{Roles: teleport.Roles{teleport.RoleNode}, Token: customToken})
c.Assert(err, IsNil)
c.Assert(tok, Equals, customToken)
@ -363,13 +362,8 @@ func (s *AuthSuite) TestBuildRolesInvalid(c *C) {
claims.Add("nickname", "foo")
claims.Add("full_name", "foo bar")
// create an identity for the ttl
ident := &oidc.Identity{
ExpiresAt: time.Now().Add(1 * time.Minute),
}
// try and build roles should be invalid since we have no mappings
_, err := s.a.buildRoles(oidcConnector, ident, claims)
_, err := s.a.buildOIDCRoles(oidcConnector, claims)
c.Assert(err, NotNil)
}
@ -398,74 +392,13 @@ func (s *AuthSuite) TestBuildRolesStatic(c *C) {
claims.Add("nickname", "foo")
claims.Add("full_name", "foo bar")
// create an identity for the ttl
ident := &oidc.Identity{
ExpiresAt: time.Now().Add(1 * time.Minute),
}
// build roles and check that we mapped to "user" role
roles, err := s.a.buildRoles(oidcConnector, ident, claims)
roles, err := s.a.buildOIDCRoles(oidcConnector, claims)
c.Assert(err, IsNil)
c.Assert(roles, HasLen, 1)
c.Assert(roles[0], Equals, "user")
}
func (s *AuthSuite) TestBuildRolesTemplate(c *C) {
// create a connector
oidcConnector := services.NewOIDCConnector("example", services.OIDCConnectorSpecV2{
IssuerURL: "https://www.exmaple.com",
ClientID: "example-client-id",
ClientSecret: "example-client-secret",
RedirectURL: "https://localhost:3080/v1/webapi/oidc/callback",
Display: "sign in with example.com",
Scope: []string{"foo", "bar"},
ClaimsToRoles: []services.ClaimMapping{
services.ClaimMapping{
Claim: "roles",
Value: "teleport-user",
RoleTemplate: &services.RoleV2{
Kind: services.KindRole,
Version: services.V2,
Metadata: services.Metadata{
Name: `{{index . "email"}}`,
Namespace: defaults.Namespace,
},
Spec: services.RoleSpecV2{
MaxSessionTTL: services.NewDuration(90 * 60 * time.Minute),
Logins: []string{`{{index . "nickname"}}`, `root`},
NodeLabels: map[string]string{"*": "*"},
Namespaces: []string{"*"},
},
},
},
},
})
// create some claims
var claims = make(jose.Claims)
claims.Add("roles", "teleport-user")
claims.Add("email", "foo@example.com")
claims.Add("nickname", "foo")
claims.Add("full_name", "foo bar")
// create an identity for the ttl
ident := &oidc.Identity{
ExpiresAt: time.Now().Add(1 * time.Minute),
}
// build roles
roles, err := s.a.buildRoles(oidcConnector, ident, claims)
c.Assert(err, IsNil)
// check that the newly created role was both returned and upserted into the backend
r, err := s.a.GetRoles()
c.Assert(err, IsNil)
c.Assert(r, HasLen, 1)
c.Assert(r[0].GetName(), Equals, "foo@example.com")
c.Assert(roles, HasLen, 1)
c.Assert(roles[0], Equals, "foo@example.com")
}
func (s *AuthSuite) TestValidateACRValues(c *C) {
var tests = []struct {

View file

@ -291,30 +291,11 @@ type OIDCAuthResponse struct {
HostSigners []services.CertAuthority `json:"host_signers"`
}
// buildRoles takes a connector and claims and returns a slice of roles. If the claims
// match a concrete roles in the connector, those roles are returned directly. If the
// claims match a template role in the connector, then that role is first created from
// the template, then returned.
func (a *AuthServer) buildRoles(connector services.OIDCConnector, ident *oidc.Identity, claims jose.Claims) ([]string, error) {
// buildOIDCRoles takes a connector and claims and returns a slice of roles.
func (a *AuthServer) buildOIDCRoles(connector services.OIDCConnector, claims jose.Claims) ([]string, error) {
roles := connector.MapClaims(claims)
if len(roles) == 0 {
role, err := connector.RoleFromTemplate(claims)
if err != nil {
log.Warningf("[OIDC] Unable to map claims to roles or role templates for %q: %v", connector.GetName(), err)
return nil, trace.AccessDenied("unable to map claims to roles or role templates for %q: %v", connector.GetName(), err)
}
// figure out ttl for role. expires = now + ttl => ttl = expires - now
ttl := ident.ExpiresAt.Sub(a.clock.Now())
// upsert templated role
err = a.Access.UpsertRole(role, ttl)
if err != nil {
log.Warningf("[OIDC] Unable to upsert templated role for connector: %q: %v", connector.GetName(), err)
return nil, trace.AccessDenied("unable to upsert templated role: %q: %v", connector.GetName(), err)
}
roles = []string{role.GetName()}
return nil, trace.AccessDenied("unable to map claims to role for connector: %v", connector.GetName())
}
return roles, nil
@ -340,7 +321,7 @@ func claimsToTraitMap(claims jose.Claims) map[string][]string {
}
func (a *AuthServer) createOIDCUser(connector services.OIDCConnector, ident *oidc.Identity, claims jose.Claims) error {
roles, err := a.buildRoles(connector, ident, claims)
roles, err := a.buildOIDCRoles(connector, claims)
if err != nil {
return trace.Wrap(err)
}

View file

@ -79,30 +79,11 @@ func (s *AuthServer) getSAMLProvider(conn services.SAMLConnector) (*saml2.SAMLSe
return serviceProvider, nil
}
// buildSAMLRoles takes a connector and claims and returns a slice of roles. If the claims
// match a concrete roles in the connector, those roles are returned directly. If the
// claims match a template role in the connector, then that role is first created from
// the template, then returned.
func (a *AuthServer) buildSAMLRoles(connector services.SAMLConnector, assertionInfo saml2.AssertionInfo, expiresAt time.Time) ([]string, error) {
// buildSAMLRoles takes a connector and claims and returns a slice of roles.
func (a *AuthServer) buildSAMLRoles(connector services.SAMLConnector, assertionInfo saml2.AssertionInfo) ([]string, error) {
roles := connector.MapAttributes(assertionInfo)
if len(roles) == 0 {
role, err := connector.RoleFromTemplate(assertionInfo)
if err != nil {
log.Warningf("[SAML] Unable to map claims to roles for %q: %v", connector.GetName(), err)
return nil, trace.AccessDenied("unable to map claims to roles for %q: %v", connector.GetName(), err)
}
// figure out ttl for role. expires = now + ttl => ttl = expires - now
ttl := expiresAt.Sub(a.clock.Now())
// upsert templated role
err = a.Access.UpsertRole(role, ttl)
if err != nil {
log.Warningf("[SAML] Unable to upsert templated role for connector: %q: %v", connector.GetName(), err)
return nil, trace.AccessDenied("unable to upsert templated role: %q: %v", connector.GetName(), err)
}
roles = []string{role.GetName()}
return nil, trace.AccessDenied("unable to map attributes to role for connector: %v", connector.GetName())
}
return roles, nil
@ -125,7 +106,7 @@ func assertionsToTraitMap(assertionInfo saml2.AssertionInfo) map[string][]string
}
func (a *AuthServer) createSAMLUser(connector services.SAMLConnector, assertionInfo saml2.AssertionInfo, expiresAt time.Time) error {
roles, err := a.buildSAMLRoles(connector, assertionInfo, expiresAt)
roles, err := a.buildSAMLRoles(connector, assertionInfo)
if err != nil {
return trace.Wrap(err)
}

View file

@ -106,11 +106,13 @@ func New(params backend.Params) (backend.Backend, error) {
}
return nil, trace.Wrap(err)
}
return &BoltBackend{
// Wrap the backend in a input sanitizer and return it.
return backend.NewSanitizer(&BoltBackend{
locks: make(map[string]time.Time),
clock: clockwork.NewRealClock(),
db: db,
}, nil
}), nil
}
// Clock returns clock assigned to the backend

View file

@ -28,7 +28,7 @@ import (
func TestBolt(t *testing.T) { TestingT(t) }
type BoltSuite struct {
bk *BoltBackend
bk backend.Backend
suite test.BackendSuite
}
@ -39,14 +39,14 @@ func (s *BoltSuite) SetUpSuite(c *C) {
}
func (s *BoltSuite) SetUpTest(c *C) {
var err error
dir := c.MkDir()
bk, err := New(backend.Params{
s.bk, err = New(backend.Params{
"path": dir,
})
c.Assert(err, IsNil)
c.Assert(bk, NotNil)
s.bk, _ = bk.(*BoltBackend)
c.Assert(s.bk, NotNil)
s.suite.ChangesC = make(chan interface{})
s.suite.B = s.bk

View file

@ -97,7 +97,9 @@ func New(params backend.Params) (backend.Backend, error) {
if err := os.MkdirAll(locksDir, defaultDirMode); err != nil {
return nil, trace.ConvertSystemError(err)
}
return bk, nil
// Wrap the backend in a input sanitizer and return it.
return backend.NewSanitizer(bk), nil
}
// GetItems is a function that returns keys in batch

View file

@ -44,16 +44,20 @@ var _ = check.Suite(&Suite{clock: clockwork.NewFakeClock()})
func TestFSBackend(t *testing.T) { check.TestingT(t) }
func (s *Suite) SetUpSuite(c *check.C) {
var err error
dirName := c.MkDir()
bk, err := New(backend.Params{"path": dirName})
bk.(*Backend).InternalClock = s.clock
s.bk, err = New(backend.Params{"path": dirName})
sb, ok := s.bk.(*backend.Sanitizer)
c.Assert(ok, check.Equals, true)
sb.Backend().(*Backend).InternalClock = s.clock
c.Assert(err, check.IsNil)
// backend must create the dir:
c.Assert(utils.IsDir(dirName), check.Equals, true)
s.bk = bk
s.suite.B = s.bk
}

View file

@ -198,7 +198,9 @@ func New(params backend.Params) (backend.Backend, error) {
if err != nil {
return nil, trace.Wrap(err)
}
return b, nil
// Wrap backend in a input sanitizer and return it.
return backend.NewSanitizer(b), nil
}
type tableStatus int

View file

@ -88,7 +88,9 @@ func New(params backend.Params) (backend.Backend, error) {
if err = b.reconnect(); err != nil {
return nil, trace.Wrap(err)
}
return b, nil
// Wrap backend in a input sanitizer and return it.
return backend.NewSanitizer(b), nil
}
// Validate checks if all the parameters are present/valid

View file

@ -58,10 +58,14 @@ func (s *EtcdSuite) SetUpSuite(c *C) {
"tls_ca_file": "../../../examples/etcd/certs/ca-cert.pem",
}
// Initiate a backend with a registry
b, err := New(s.config)
raw, err := New(s.config)
c.Assert(err, IsNil)
s.bk = b.(*bk)
s.suite.B = b
sb, ok := raw.(*backend.Sanitizer)
c.Assert(ok, Equals, true)
s.bk = sb.Backend().(*bk)
s.suite.B = raw
}
func (s *EtcdSuite) SetUpTest(c *C) {

174
lib/backend/sanitize.go Normal file
View file

@ -0,0 +1,174 @@
package backend
import (
"path/filepath"
"regexp"
"strings"
"time"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
)
// errorMessage is the error message to return when invalid input is provided by the caller.
const errorMessage = "special characters are not allowed in resource names, please use name composed only from characters, hyphens and dots"
// whitelistPattern is the pattern of allowed characters for each key within
// the path.
var whitelistPattern = regexp.MustCompile(`^[0-9A-Za-z@_:.-]*$`)
// isStringSafe checks if the passed in string conforms to the whitelist.
func isStringSafe(s string) bool {
if strings.Contains(s, "..") {
return false
}
if strings.Contains(s, string(filepath.Separator)) {
return false
}
return whitelistPattern.MatchString(s)
}
// isSliceSafe checks if the passed in slice conforms to the whitelist.
func isSliceSafe(slice []string) bool {
for _, s := range slice {
if !isStringSafe(s) {
return false
}
}
return true
}
// Sanitizer wraps a Backend implementation to make sure all values requested
// of the backend are whitelisted.
type Sanitizer struct {
backend Backend
}
// NewSanitizer returns a new Sanitizer.
func NewSanitizer(backend Backend) *Sanitizer {
return &Sanitizer{
backend: backend,
}
}
// Backend returns the underlying backend. Useful when knowing the type of
// backend is important (for example, can the backend support forking).
func (s *Sanitizer) Backend() Backend {
return s.backend
}
// GetKeys returns a list of keys for a given path.
func (s *Sanitizer) GetKeys(bucket []string) ([]string, error) {
if !isSliceSafe(bucket) {
return nil, trace.BadParameter(errorMessage)
}
return s.backend.GetKeys(bucket)
}
// CreateVal creates value with a given TTL and key in the bucket. If the
// value already exists, returns trace.AlreadyExistsError.
func (s *Sanitizer) CreateVal(bucket []string, key string, val []byte, ttl time.Duration) error {
if !isSliceSafe(bucket) {
return trace.BadParameter(errorMessage)
}
if !isStringSafe(key) {
return trace.BadParameter(errorMessage)
}
return s.backend.CreateVal(bucket, key, val, ttl)
}
// UpsertVal updates or inserts value with a given TTL into a bucket. Use
// backend.ForeverTTL for no TTL.
func (s *Sanitizer) UpsertVal(bucket []string, key string, val []byte, ttl time.Duration) error {
if !isSliceSafe(bucket) {
return trace.BadParameter(errorMessage)
}
if !isStringSafe(key) {
return trace.BadParameter(errorMessage)
}
return s.backend.UpsertVal(bucket, key, val, ttl)
}
// GetVal returns a value for a given key in the bucket.
func (s *Sanitizer) GetVal(bucket []string, key string) ([]byte, error) {
if !isSliceSafe(bucket) {
return nil, trace.BadParameter(errorMessage)
}
if !isStringSafe(key) {
return nil, trace.BadParameter(errorMessage)
}
return s.backend.GetVal(bucket, key)
}
// CompareAndSwapVal compares and swaps values in atomic operation, succeeds
// if prevVal matches the value stored in the database, requires prevVal as a
// non-empty value. Returns trace.CompareFailed in case if value did not match.
func (s *Sanitizer) CompareAndSwapVal(bucket []string, key string, val []byte, prevVal []byte, ttl time.Duration) error {
if !isSliceSafe(bucket) {
return trace.BadParameter(errorMessage)
}
if !isStringSafe(key) {
return trace.BadParameter(errorMessage)
}
return s.backend.CompareAndSwapVal(bucket, key, val, prevVal, ttl)
}
// DeleteKey deletes a key in a bucket.
func (s *Sanitizer) DeleteKey(bucket []string, key string) error {
if !isSliceSafe(bucket) {
return trace.BadParameter(errorMessage)
}
if !isStringSafe(key) {
return trace.BadParameter(errorMessage)
}
return s.backend.DeleteKey(bucket, key)
}
// DeleteBucket deletes the bucket by a given path.
func (s *Sanitizer) DeleteBucket(path []string, bucket string) error {
if !isSliceSafe(path) {
return trace.BadParameter(errorMessage)
}
if !isStringSafe(bucket) {
return trace.BadParameter(errorMessage)
}
return s.backend.DeleteBucket(path, bucket)
}
// AcquireLock grabs a lock that will be released automatically after a TTL.
func (s *Sanitizer) AcquireLock(token string, ttl time.Duration) error {
if !isStringSafe(token) {
return trace.BadParameter(errorMessage)
}
return s.backend.AcquireLock(token, ttl)
}
// ReleaseLock forces lock release before the TTL has expired.
func (s *Sanitizer) ReleaseLock(token string) error {
if !isStringSafe(token) {
return trace.BadParameter(errorMessage)
}
return s.backend.ReleaseLock(token)
}
// Close releases the resources taken up by this backend
func (s *Sanitizer) Close() error {
return s.backend.Close()
}
// Clock returns clock used by this backend
func (s *Sanitizer) Clock() clockwork.Clock {
return s.backend.Clock()
}

View file

@ -0,0 +1,144 @@
package backend
import (
"fmt"
"testing"
"time"
"github.com/jonboulle/clockwork"
"gopkg.in/check.v1"
)
func TestSanitizer(t *testing.T) { check.TestingT(t) }
type Suite struct {
}
var _ = check.Suite(&Suite{})
var _ = fmt.Printf
func (s *Suite) SetUpSuite(c *check.C) {
}
func (s *Suite) TearDownSuite(c *check.C) {
}
func (s *Suite) TearDownTest(c *check.C) {
}
func (s *Suite) SetUpTest(c *check.C) {
}
func (s *Suite) TestSanitizeBucket(c *check.C) {
tests := []struct {
inBucket []string
inKey string
outError bool
}{
{
inBucket: []string{"foo", "bar", "../../../etc/passwd"},
inKey: "",
outError: true,
},
{
inBucket: []string{},
inKey: "../../../etc/passwd",
outError: true,
},
{
inBucket: []string{"foo", "bar", "../../../etc/passwd"},
inKey: "../../../etc/passwd",
outError: true,
},
{
inBucket: []string{"foo", "bar"},
inKey: "baz-foo:bar.com",
outError: false,
},
}
for i, tt := range tests {
comment := check.Commentf("Test %v", i)
safeBackend := NewSanitizer(&nopBackend{})
if len(tt.inBucket) != 0 {
_, err := safeBackend.GetKeys(tt.inBucket)
c.Assert(err != nil, check.Equals, tt.outError, comment)
err = safeBackend.CreateVal(tt.inBucket, tt.inKey, []byte{}, Forever)
c.Assert(err != nil, check.Equals, tt.outError, comment)
err = safeBackend.UpsertVal(tt.inBucket, tt.inKey, []byte{}, Forever)
c.Assert(err != nil, check.Equals, tt.outError, comment)
_, err = safeBackend.GetVal(tt.inBucket, tt.inKey)
c.Assert(err != nil, check.Equals, tt.outError, comment)
err = safeBackend.CompareAndSwapVal(tt.inBucket, tt.inKey, []byte{}, []byte{}, Forever)
c.Assert(err != nil, check.Equals, tt.outError, comment)
err = safeBackend.DeleteKey(tt.inBucket, tt.inKey)
c.Assert(err != nil, check.Equals, tt.outError, comment)
err = safeBackend.DeleteBucket(tt.inBucket, tt.inKey)
c.Assert(err != nil, check.Equals, tt.outError, comment)
}
if tt.inKey != "" {
err := safeBackend.AcquireLock(tt.inKey, Forever)
c.Assert(err != nil, check.Equals, tt.outError, comment)
err = safeBackend.ReleaseLock(tt.inKey)
c.Assert(err != nil, check.Equals, tt.outError, comment)
}
}
}
type nopBackend struct {
}
func (n *nopBackend) GetKeys(bucket []string) ([]string, error) {
return []string{"foo"}, nil
}
func (n *nopBackend) CreateVal(bucket []string, key string, val []byte, ttl time.Duration) error {
return nil
}
func (n *nopBackend) UpsertVal(bucket []string, key string, val []byte, ttl time.Duration) error {
return nil
}
func (n *nopBackend) GetVal(path []string, key string) ([]byte, error) {
return []byte("foo"), nil
}
func (n *nopBackend) CompareAndSwapVal(bucket []string, key string, val []byte, prevVal []byte, ttl time.Duration) error {
return nil
}
func (n *nopBackend) DeleteKey(bucket []string, key string) error {
return nil
}
func (n *nopBackend) DeleteBucket(path []string, bkt string) error {
return nil
}
func (n *nopBackend) AcquireLock(token string, ttl time.Duration) error {
return nil
}
func (n *nopBackend) ReleaseLock(token string) error {
return nil
}
func (n *nopBackend) Close() error {
return nil
}
func (n *nopBackend) Clock() clockwork.Clock {
return clockwork.NewFakeClock()
}

View file

@ -35,7 +35,7 @@ func TestSessions(t *testing.T) { TestingT(t) }
type SessionSuite struct {
dir string
srv *server
bk *dir.Backend
bk backend.Backend
clock clockwork.FakeClock
}
@ -46,13 +46,18 @@ func (s *SessionSuite) SetUpSuite(c *C) {
}
func (s *SessionSuite) SetUpTest(c *C) {
var err error
s.clock = clockwork.NewFakeClockAt(time.Date(2016, 9, 8, 7, 6, 5, 0, time.UTC))
s.dir = c.MkDir()
bk, err := dir.New(backend.Params{"path": s.dir})
s.bk, err = dir.New(backend.Params{"path": s.dir})
c.Assert(err, IsNil)
s.bk = bk.(*dir.Backend)
s.bk.InternalClock = s.clock
sb, ok := s.bk.(*backend.Sanitizer)
c.Assert(ok, Equals, true)
sb.Backend().(*dir.Backend).InternalClock = s.clock
srv, err := New(s.bk)
s.srv = srv.(*server)