frostfs-node/pkg/services/object/acl/v2/util.go
Leonard Lyubich e54b52ec03 [#1420] object/acl: Fix correlation of object session to request
In previous implementation of `neofs-node` app object session was not
checked for substitution of the object related to it. Also, for access
checks, the session object was substituted instead of the one from the
request. This, on the one hand, made it possible to inherit the session
from the parent object for authorization for certain actions. On the
other hand, it covered the mentioned object substitution, which is a
critical vulnerability.

Next changes are applied to processing of all Object service requests:
 - check if object session relates to the requested object
 - use requested object in access checks.

Disclosed problem of object context inheritance will be solved within

Signed-off-by: Leonard Lyubich <ctulhurider@gmail.com>
2022-10-07 10:34:38 +03:00

214 lines
6.1 KiB
Go

package v2
import (
"crypto/ecdsa"
"crypto/elliptic"
"errors"
"fmt"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object"
refsV2 "github.com/nspcc-dev/neofs-api-go/v2/refs"
sessionV2 "github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-sdk-go/bearer"
"github.com/nspcc-dev/neofs-sdk-go/container/acl"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
sessionSDK "github.com/nspcc-dev/neofs-sdk-go/session"
"github.com/nspcc-dev/neofs-sdk-go/user"
)
var errMissingContainerID = errors.New("missing container ID")
func getContainerIDFromRequest(req interface{}) (cid.ID, error) {
var idV2 *refsV2.ContainerID
var id cid.ID
switch v := req.(type) {
case *objectV2.GetRequest:
idV2 = v.GetBody().GetAddress().GetContainerID()
case *objectV2.PutRequest:
part, ok := v.GetBody().GetObjectPart().(*objectV2.PutObjectPartInit)
if !ok {
return cid.ID{}, errors.New("can't get container ID in chunk")
}
idV2 = part.GetHeader().GetContainerID()
case *objectV2.HeadRequest:
idV2 = v.GetBody().GetAddress().GetContainerID()
case *objectV2.SearchRequest:
idV2 = v.GetBody().GetContainerID()
case *objectV2.DeleteRequest:
idV2 = v.GetBody().GetAddress().GetContainerID()
case *objectV2.GetRangeRequest:
idV2 = v.GetBody().GetAddress().GetContainerID()
case *objectV2.GetRangeHashRequest:
idV2 = v.GetBody().GetAddress().GetContainerID()
default:
return cid.ID{}, errors.New("unknown request type")
}
if idV2 == nil {
return cid.ID{}, errMissingContainerID
}
return id, id.ReadFromV2(*idV2)
}
// originalBearerToken goes down to original request meta header and fetches
// bearer token from there.
func originalBearerToken(header *sessionV2.RequestMetaHeader) (*bearer.Token, error) {
for header.GetOrigin() != nil {
header = header.GetOrigin()
}
tokV2 := header.GetBearerToken()
if tokV2 == nil {
return nil, nil
}
var tok bearer.Token
return &tok, tok.ReadFromV2(*tokV2)
}
// originalSessionToken goes down to original request meta header and fetches
// session token from there.
func originalSessionToken(header *sessionV2.RequestMetaHeader) (*sessionSDK.Object, error) {
for header.GetOrigin() != nil {
header = header.GetOrigin()
}
tokV2 := header.GetSessionToken()
if tokV2 == nil {
return nil, nil
}
var tok sessionSDK.Object
err := tok.ReadFromV2(*tokV2)
if err != nil {
return nil, fmt.Errorf("invalid session token: %w", err)
}
return &tok, nil
}
// getObjectIDFromRequestBody decodes oid.ID from the common interface of the
// object reference's holders. Returns an error if object ID is missing in the request.
func getObjectIDFromRequestBody(body interface{ GetAddress() *refsV2.Address }) (*oid.ID, error) {
idV2 := body.GetAddress().GetObjectID()
if idV2 == nil {
return nil, errors.New("missing object ID")
}
var id oid.ID
err := id.ReadFromV2(*idV2)
if err != nil {
return nil, err
}
return &id, nil
}
func ownerFromToken(token *sessionSDK.Object) (*user.ID, *keys.PublicKey, error) {
// 1. First check signature of session token.
if !token.VerifySignature() {
return nil, nil, fmt.Errorf("%w: invalid session token signature", ErrMalformedRequest)
}
// 2. Then check if session token owner issued the session token
// TODO(@cthulhu-rider): #1387 implement and use another approach to avoid conversion
var tokV2 sessionV2.Token
token.WriteToV2(&tokV2)
tokenIssuerKey, err := unmarshalPublicKey(tokV2.GetSignature().GetKey())
if err != nil {
return nil, nil, fmt.Errorf("invalid key in session token signature: %w", err)
}
tokenIssuer := token.Issuer()
if !isOwnerFromKey(tokenIssuer, tokenIssuerKey) {
// TODO: #767 in this case we can issue all owner keys from neofs.id and check once again
return nil, nil, fmt.Errorf("%w: invalid session token owner", ErrMalformedRequest)
}
return &tokenIssuer, tokenIssuerKey, nil
}
func originalBodySignature(v *sessionV2.RequestVerificationHeader) *refsV2.Signature {
if v == nil {
return nil
}
for v.GetOrigin() != nil {
v = v.GetOrigin()
}
return v.GetBodySignature()
}
func unmarshalPublicKey(bs []byte) (*keys.PublicKey, error) {
return keys.NewPublicKeyFromBytes(bs, elliptic.P256())
}
func isOwnerFromKey(id user.ID, key *keys.PublicKey) bool {
if key == nil {
return false
}
var id2 user.ID
user.IDFromKey(&id2, (ecdsa.PublicKey)(*key))
return id2.Equals(id)
}
// assertVerb checks that token verb corresponds to op.
func assertVerb(tok sessionSDK.Object, op acl.Op) bool {
//nolint:exhaustive
switch op {
case acl.OpObjectPut:
return tok.AssertVerb(sessionSDK.VerbObjectPut, sessionSDK.VerbObjectDelete)
case acl.OpObjectDelete:
return tok.AssertVerb(sessionSDK.VerbObjectDelete)
case acl.OpObjectGet:
return tok.AssertVerb(sessionSDK.VerbObjectGet)
case acl.OpObjectHead:
return tok.AssertVerb(
sessionSDK.VerbObjectHead,
sessionSDK.VerbObjectGet,
sessionSDK.VerbObjectDelete,
sessionSDK.VerbObjectRange,
sessionSDK.VerbObjectRangeHash)
case acl.OpObjectSearch:
return tok.AssertVerb(sessionSDK.VerbObjectSearch, sessionSDK.VerbObjectDelete)
case acl.OpObjectRange:
return tok.AssertVerb(sessionSDK.VerbObjectRange, sessionSDK.VerbObjectRangeHash)
case acl.OpObjectHash:
return tok.AssertVerb(sessionSDK.VerbObjectRangeHash)
}
return false
}
// assertSessionRelation checks if given token describing the NeoFS session
// relates to the given container and optional object. Missing object
// means that the context isn't bound to any NeoFS object in the container.
// Returns no error iff relation is correct. Criteria:
//
// session is bound to the given container
// object is not specified or session is bound to this object
//
// Session MUST be bound to the particular container, otherwise behavior is undefined.
func assertSessionRelation(tok sessionSDK.Object, cnr cid.ID, obj *oid.ID) error {
if !tok.AssertContainer(cnr) {
return errors.New("requested container is not related to the session")
}
if obj != nil && !tok.AssertObject(*obj) {
return errors.New("requested object is not related to the session")
}
return nil
}