Skip to content

Commit 877d7e3

Browse files
authored
Fix digest auth (#789)
* fix: handle commas in digest challenge values Reimplement parsing of digest auth challenge to handle cases where the values of key/value pairs contain commas, such as in qop="auth, auth-int" * fix: digest auth handle qop values separated without spaces The digest auth qop validation did only handle values separated like "auth, auth-int", but not "auth,auth-int".
1 parent 733395c commit 877d7e3

File tree

3 files changed

+70
-33
lines changed

3 files changed

+70
-33
lines changed

client_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ func TestClientDigestAuth(t *testing.T) {
104104
func TestClientDigestSession(t *testing.T) {
105105
conf := defaultDigestServerConf()
106106
conf.algo = "MD5-sess"
107+
conf.qop = "auth, auth-int"
107108
ts := createDigestServer(t, conf)
108109
defer ts.Close()
109110

@@ -134,6 +135,7 @@ func TestClientDigestErrors(t *testing.T) {
134135
{mutateConf: func(c *digestServerConfig) { c.uri = "/bad" }, expect: ErrDigestBadChallenge},
135136
{mutateConf: func(c *digestServerConfig) { c.uri = "/unknown_param" }, expect: ErrDigestBadChallenge},
136137
{mutateConf: func(c *digestServerConfig) { c.uri = "/missing_value" }, expect: ErrDigestBadChallenge},
138+
{mutateConf: func(c *digestServerConfig) { c.uri = "/unclosed_quote" }, expect: ErrDigestBadChallenge},
137139
{mutateConf: func(c *digestServerConfig) { c.uri = "/no_challenge" }, expect: ErrDigestBadChallenge},
138140
{mutateConf: func(c *digestServerConfig) { c.uri = "/status_500" }, expect: nil},
139141
}

digest.go

Lines changed: 65 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) 2015-2023 Jeevanandam M ([email protected])
22
// 2023 Segev Dagan (https://github.com/segevda)
3+
// 2024 Philipp Wolfer (https://github.com/phw)
34
// All rights reserved.
45
// resty source code and usage is governed by a MIT style
56
// license that can be found in the LICENSE file.
@@ -125,48 +126,78 @@ type challenge struct {
125126
userhash string
126127
}
127128

129+
func (c *challenge) setValue(k, v string) error {
130+
switch k {
131+
case "realm":
132+
c.realm = v
133+
case "domain":
134+
c.domain = v
135+
case "nonce":
136+
c.nonce = v
137+
case "opaque":
138+
c.opaque = v
139+
case "stale":
140+
c.stale = v
141+
case "algorithm":
142+
c.algorithm = v
143+
case "qop":
144+
c.qop = v
145+
case "charset":
146+
if strings.ToUpper(v) != "UTF-8" {
147+
return ErrDigestCharset
148+
}
149+
case "userhash":
150+
c.userhash = v
151+
default:
152+
return ErrDigestBadChallenge
153+
}
154+
return nil
155+
}
156+
128157
func parseChallenge(input string) (*challenge, error) {
129158
const ws = " \n\r\t"
130-
const qs = `"`
131159
s := strings.Trim(input, ws)
132160
if !strings.HasPrefix(s, "Digest ") {
133161
return nil, ErrDigestBadChallenge
134162
}
135163
s = strings.Trim(s[7:], ws)
136-
sl := strings.Split(s, ",")
137164
c := &challenge{}
138-
var r []string
139-
for i := range sl {
140-
sl[i] = strings.TrimSpace(sl[i])
141-
r = strings.SplitN(sl[i], "=", 2)
142-
if len(r) != 2 {
143-
return nil, ErrDigestBadChallenge
144-
}
145-
r[0] = strings.TrimSpace(r[0])
146-
r[1] = strings.TrimSpace(r[1])
147-
switch r[0] {
148-
case "realm":
149-
c.realm = strings.Trim(r[1], qs)
150-
case "domain":
151-
c.domain = strings.Trim(r[1], qs)
152-
case "nonce":
153-
c.nonce = strings.Trim(r[1], qs)
154-
case "opaque":
155-
c.opaque = strings.Trim(r[1], qs)
156-
case "stale":
157-
c.stale = strings.Trim(r[1], qs)
158-
case "algorithm":
159-
c.algorithm = strings.Trim(r[1], qs)
160-
case "qop":
161-
c.qop = strings.Trim(r[1], qs)
162-
case "charset":
163-
if strings.ToUpper(strings.Trim(r[1], qs)) != "UTF-8" {
164-
return nil, ErrDigestCharset
165+
b := strings.Builder{}
166+
key := ""
167+
quoted := false
168+
for _, r := range s {
169+
switch r {
170+
case '"':
171+
quoted = !quoted
172+
case ',':
173+
if quoted {
174+
b.WriteRune(r)
175+
} else {
176+
val := strings.Trim(b.String(), ws)
177+
b.Reset()
178+
if err := c.setValue(key, val); err != nil {
179+
return nil, err
180+
}
181+
key = ""
182+
}
183+
case '=':
184+
if quoted {
185+
b.WriteRune(r)
186+
} else {
187+
key = strings.Trim(b.String(), ws)
188+
b.Reset()
165189
}
166-
case "userhash":
167-
c.userhash = strings.Trim(r[1], qs)
168190
default:
169-
return nil, ErrDigestBadChallenge
191+
b.WriteRune(r)
192+
}
193+
}
194+
if quoted || (key == "" && b.Len() > 0) {
195+
return nil, ErrDigestBadChallenge
196+
}
197+
if key != "" {
198+
val := strings.Trim(b.String(), ws)
199+
if err := c.setValue(key, val); err != nil {
200+
return nil, err
170201
}
171202
}
172203
return c, nil
@@ -233,9 +264,10 @@ func (c *credentials) validateQop() error {
233264
if c.messageQop == "" {
234265
return ErrDigestNoQop
235266
}
236-
possibleQops := strings.Split(c.messageQop, ", ")
267+
possibleQops := strings.Split(c.messageQop, ",")
237268
var authSupport bool
238269
for _, qop := range possibleQops {
270+
qop = strings.TrimSpace(qop)
239271
if qop == "auth" {
240272
authSupport = true
241273
break

resty_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,9 @@ func createDigestServer(t *testing.T, conf *digestServerConfig) *httptest.Server
700700
case "/missing_value":
701701
setWWWAuthHeader(w, `Digest realm="hello", domain`)
702702
return
703+
case "/unclosed_quote":
704+
setWWWAuthHeader(w, `Digest realm="hello, qop=auth`)
705+
return
703706
case "/no_challenge":
704707
setWWWAuthHeader(w, "")
705708
return

0 commit comments

Comments
 (0)