Skip to content

Commit 0130619

Browse files
Merge pull request #37 from torbensky/comment-injection-test
Unit test for comment injection attack
2 parents 319306b + a962dc7 commit 0130619

3 files changed

Lines changed: 82 additions & 20 deletions

File tree

decode_response.go

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,7 @@ func (sp *SAMLServiceProvider) decryptAssertions(el *etree.Element) error {
136136
return fmt.Errorf("unable to decrypt encrypted assertion: %v", derr)
137137
}
138138

139-
doc := etree.NewDocument()
140-
err = doc.ReadFromBytes(raw)
139+
doc, _, err := parseResponse(raw)
141140
if err != nil {
142141
return fmt.Errorf("unable to create element from decrypted assertion bytes: %v", derr)
143142
}
@@ -218,25 +217,10 @@ func (sp *SAMLServiceProvider) ValidateEncodedResponse(encodedResponse string) (
218217
return nil, err
219218
}
220219

221-
doc := etree.NewDocument()
222-
err = doc.ReadFromBytes(raw)
220+
// Parse the raw response
221+
doc, el, err := parseResponse(raw)
223222
if err != nil {
224-
// Attempt to inflate the response in case it happens to be compressed (as with one case at saml.oktadev.com)
225-
buf, err := ioutil.ReadAll(flate.NewReader(bytes.NewReader(raw)))
226-
if err != nil {
227-
return nil, err
228-
}
229-
230-
doc = etree.NewDocument()
231-
err = doc.ReadFromBytes(buf)
232-
if err != nil {
233-
return nil, err
234-
}
235-
}
236-
237-
el := doc.Root()
238-
if el == nil {
239-
return nil, fmt.Errorf("unable to parse response")
223+
return nil, err
240224
}
241225

242226
var responseSignatureValidated bool
@@ -292,3 +276,29 @@ func (sp *SAMLServiceProvider) ValidateEncodedResponse(encodedResponse string) (
292276

293277
return decodedResponse, nil
294278
}
279+
280+
// parseResponse is a helper function that was refactored out so that the XML parsing behavior can be isolated and unit tested
281+
func parseResponse(xml []byte) (*etree.Document, *etree.Element, error) {
282+
doc := etree.NewDocument()
283+
err := doc.ReadFromBytes(xml)
284+
if err != nil {
285+
// Attempt to inflate the response in case it happens to be compressed (as with one case at saml.oktadev.com)
286+
buf, err := ioutil.ReadAll(flate.NewReader(bytes.NewReader(xml)))
287+
if err != nil {
288+
return nil, nil, err
289+
}
290+
291+
doc = etree.NewDocument()
292+
err = doc.ReadFromBytes(buf)
293+
if err != nil {
294+
return nil, nil, err
295+
}
296+
}
297+
298+
el := doc.Root()
299+
if el == nil {
300+
return nil, nil, fmt.Errorf("unable to parse response")
301+
}
302+
303+
return doc, el, nil
304+
}

saml_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"compress/flate"
1212

1313
"github.com/beevik/etree"
14+
"github.com/russellhaering/gosaml2/types"
1415
"github.com/russellhaering/goxmldsig"
1516
require "github.com/stretchr/testify/require"
1617
)
@@ -253,3 +254,35 @@ func TestInvalidResponseNoElement(t *testing.T) {
253254
require.EqualError(t, err, "unable to parse response")
254255
require.Nil(t, response)
255256
}
257+
func TestSAMLCommentInjection(t *testing.T) {
258+
/*
259+
Explanation:
260+
261+
See: https://duo.com/blog/duo-finds-saml-vulnerabilities-affecting-multiple-implementations
262+
263+
The TLDR is that XML canonicalization may result in a different value being signed from the one being retrieved.
264+
The target of this is the NameID in the Subject of the SAMLResponse Assertion
265+
266+
Example:
267+
The following Subject
268+
```<Subject>
269+
<NameID>user@user.com<!---->.evil.com</NameID>
270+
</Subject>```
271+
would get canonicalized to
272+
```
273+
<Subject>
274+
<NameID>user@user.com.evil.com</NameID>
275+
</Subject>
276+
```
277+
Many XML parsers have a behavior where they pull the first text element, so in the example with the comment, a vulnerable XML parser would return `user@user.com`, ignoring the text after the comment.
278+
Knowing this, a user (user@user.com.evil.com) can attack a vulnerable SP by manipulating their signed SAMLResponse with a comment that turns their username into another one.
279+
*/
280+
281+
// To show that we are not vulnerable, we want to prove that we get the canonicalized value using our parser
282+
_, el, err := parseResponse([]byte(commentInjectionAttackResponse))
283+
require.NoError(t, err)
284+
decodedResponse := &types.Response{}
285+
err = xmlUnmarshalElement(el, decodedResponse)
286+
require.NoError(t, err)
287+
require.Equal(t, "phoebe.simon@scaleft.com.evil.com", decodedResponse.Assertions[0].Subject.NameID.Value, "The full, canonacalized NameID should be returned.")
288+
}

0 commit comments

Comments
 (0)