connector/saml: fix validation bug with multiple Assertion elements
When a SAML response provided multiple Assertion elements, only the first one is checked for a valid signature. If the Assertion is verified, the original Assertion is removed and the canonicalized version is prepended to the Response. However, if there were multiple assertions, the second assertion could end up first in the list of Assertions, even if it was unsigned. For example this: <Response> <!-- Response unsigned. According to SAML spec must check assertion signature. --> <Assertion> <Signature> <!-- Correrctly signed assertion --> </Signature> </Assertion> <Assertion> <!-- Unsigned assertion inserted by attacker--> </Assertion> </Response> could be verified then re-ordered to the following: <Response> <!-- Response unsigned. According to SAML spec must check assertion signature. --> <Assertion> <!-- Unsigned assertion inserted by attacker--> </Assertion> <Assertion> <!-- Canonicalized, correrctly signed assertion --> </Assertion> </Response> Fix this by removing all unverified child elements of the Response, not just the original assertion.
This commit is contained in:
parent
f4865a354c
commit
e0709dc2ac
1 changed files with 57 additions and 38 deletions
|
@ -130,9 +130,7 @@ func (c *Config) Open(logger logrus.FieldLogger) (connector.Connector, error) {
|
||||||
return c.openConnector(logger)
|
return c.openConnector(logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) openConnector(logger logrus.FieldLogger) (interface {
|
func (c *Config) openConnector(logger logrus.FieldLogger) (*provider, error) {
|
||||||
connector.SAMLConnector
|
|
||||||
}, error) {
|
|
||||||
requiredFields := []struct {
|
requiredFields := []struct {
|
||||||
name, val string
|
name, val string
|
||||||
}{
|
}{
|
||||||
|
@ -274,8 +272,11 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ident, fmt.Errorf("decode response: %v", err)
|
return ident, fmt.Errorf("decode response: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rootElementSigned := true
|
||||||
if p.validator != nil {
|
if p.validator != nil {
|
||||||
if rawResp, err = verify(p.validator, rawResp); err != nil {
|
rawResp, rootElementSigned, err = verifyResponseSig(p.validator, rawResp)
|
||||||
|
if err != nil {
|
||||||
return ident, fmt.Errorf("verify signature: %v", err)
|
return ident, fmt.Errorf("verify signature: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -285,6 +286,7 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
|
||||||
return ident, fmt.Errorf("unmarshal response: %v", err)
|
return ident, fmt.Errorf("unmarshal response: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rootElementSigned {
|
||||||
if p.issuer != "" && resp.Issuer != nil && resp.Issuer.Issuer != p.issuer {
|
if p.issuer != "" && resp.Issuer != nil && resp.Issuer.Issuer != p.issuer {
|
||||||
return ident, fmt.Errorf("expected Issuer value %s, got %s", p.issuer, resp.Issuer.Issuer)
|
return ident, fmt.Errorf("expected Issuer value %s, got %s", p.issuer, resp.Issuer.Issuer)
|
||||||
}
|
}
|
||||||
|
@ -304,6 +306,7 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
|
||||||
if err = p.validateStatus(&resp); err != nil {
|
if err = p.validateStatus(&resp); err != nil {
|
||||||
return ident, err
|
return ident, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
assertion := resp.Assertion
|
assertion := resp.Assertion
|
||||||
if assertion == nil {
|
if assertion == nil {
|
||||||
|
@ -481,41 +484,57 @@ func (p *provider) validateConditions(assertion *assertion) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify checks the signature info of a XML document and returns
|
// verifyResponseSig attempts to verify the signature of a SAML response or
|
||||||
// the signed elements.
|
// the assertion.
|
||||||
// The Validate function of the goxmldsig library only looks for
|
//
|
||||||
// signatures on the root element level. But a saml Response is valid
|
// If the root element is properly signed, this method returns it.
|
||||||
// if the complete message is signed, or only the Assertion is signed,
|
//
|
||||||
// or but elements are signed. Therefore we first check a possible
|
// The SAML spec requires supporting responses where the root element is
|
||||||
// signature of the Response than of the Assertion. If one of these
|
// unverified, but the sub <Assertion> elements are signed. In these cases,
|
||||||
// is successful the Response is considered as valid.
|
// this method returns rootVerified=false to indicate that the <Assertion>
|
||||||
func verify(validator *dsig.ValidationContext, data []byte) (signed []byte, err error) {
|
// elements should be trusted, but all other elements MUST be ignored.
|
||||||
|
//
|
||||||
|
// Note: we still don't support multiple <Assertion> tags. If there are
|
||||||
|
// multiple present this code will only process the first.
|
||||||
|
func verifyResponseSig(validator *dsig.ValidationContext, data []byte) (signed []byte, rootVerified bool, err error) {
|
||||||
doc := etree.NewDocument()
|
doc := etree.NewDocument()
|
||||||
if err = doc.ReadFromBytes(data); err != nil {
|
if err = doc.ReadFromBytes(data); err != nil {
|
||||||
return nil, fmt.Errorf("parse document: %v", err)
|
return nil, false, fmt.Errorf("parse document: %v", err)
|
||||||
}
|
}
|
||||||
verified := false
|
|
||||||
response := doc.Root()
|
response := doc.Root()
|
||||||
transformedResponse, err := validator.Validate(response)
|
transformedResponse, err := validator.Validate(response)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
verified = true
|
// Root element is verified, return it.
|
||||||
doc.SetRoot(transformedResponse)
|
doc.SetRoot(transformedResponse)
|
||||||
|
signed, err = doc.WriteToBytes()
|
||||||
|
return signed, true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensures xmlns are copied down to the assertion element when they are defined in the root
|
// Ensures xmlns are copied down to the assertion element when they are defined in the root
|
||||||
|
//
|
||||||
|
// TODO: Only select from child elements of the root.
|
||||||
assertion, err := etreeutils.NSSelectOne(response, "urn:oasis:names:tc:SAML:2.0:assertion", "Assertion")
|
assertion, err := etreeutils.NSSelectOne(response, "urn:oasis:names:tc:SAML:2.0:assertion", "Assertion")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("response does not contain an Assertion element")
|
return nil, false, fmt.Errorf("response does not contain an Assertion element")
|
||||||
}
|
}
|
||||||
transformedAssertion, err := validator.Validate(assertion)
|
transformedAssertion, err := validator.Validate(assertion)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
verified = true
|
return nil, false, fmt.Errorf("response does not contain a valid signature element: %v", err)
|
||||||
response.RemoveChild(assertion)
|
}
|
||||||
|
|
||||||
|
// Verified an assertion but not the response. Can't trust any child elements,
|
||||||
|
// except the assertion. Remove them all.
|
||||||
|
for _, el := range response.ChildElements() {
|
||||||
|
response.RemoveChild(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We still return the full <Response> element, even though it's unverified
|
||||||
|
// because the <Assertion> element is not a valid XML document on its own.
|
||||||
|
// It still requires the root element to define things like namespaces.
|
||||||
response.AddChild(transformedAssertion)
|
response.AddChild(transformedAssertion)
|
||||||
}
|
signed, err = doc.WriteToBytes()
|
||||||
if verified != true {
|
return signed, false, err
|
||||||
return nil, fmt.Errorf("response does not contain a valid Signature element")
|
|
||||||
}
|
|
||||||
return doc.WriteToBytes()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func uuidv4() string {
|
func uuidv4() string {
|
||||||
|
|
Reference in a new issue