First drop of improved digest auth parser. Focused on the parser & input validation, but as you can see there is further room for improvement by moving more processing over to String.
The state of the old parser was rather scary.. not even comparing field names correctly and several other ugly things... Comments are very welcome while I validate the parser changes. Expect to submit this for merge in a day or two. A note of warning: This changes the quoted-string parser to actually parse quoted-string.. which impacts the Surrogate-Control parser. If that uses the parsed value to construct a new header then we also need to make sure to properly produce quoted-string as the value is now normalized as a token (quotes & escapes removed) where it before just har the quotes removed. Regards Henrik
# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: [email protected]\ # xhabeaopfdvbi372 # target_branch: ../trunk/ # testament_sha1: 9ef7f250de84e6076ea7f172491f9695736e71f4 # timestamp: 2010-03-02 17:59:33 +0100 # base_revision_id: [email protected]\ # 5miwqblkua9c5gzq # # Begin patch === modified file 'src/HttpHeaderTools.cc' --- src/HttpHeaderTools.cc 2009-09-10 03:08:35 +0000 +++ src/HttpHeaderTools.cc 2010-03-02 14:08:22 +0000 @@ -335,25 +335,29 @@ { const char *end, *pos; val->clean(); - assert (*start == '"'); + if (*start != '"') { + debugs(66, 2, "failed to parse a quoted-string header field near '" << start << "'"); + return 0; + } pos = start + 1; - while (1) { - if (!(end = index (pos,'"'))) { - debugs(66, 2, "failed to parse a quoted-string header field near '" << start << "'"); - return 0; - } - - /* check for quoted-chars */ - if (*(end - 1) != '\\') { - /* done */ - val->append(start + 1, end-start-1); - return 1; - } - - /* try for the end again */ - pos = end + 1; + while (*pos != '"') { + if (*pos == '\\') { + pos++; + } + if (!*pos) { + debugs(66, 2, "failed to parse a quoted-string header field near '" << start << "'"); + val->clean(); + return 0; + } + end = pos + strcspn(pos, "\"\\"); + val->append(pos, end-pos); + pos = end; } + /* Make sure it's defined even if empty "" */ + if (!val->defined()) + val->limitInit("", 0); + return 1; } /** === modified file 'src/auth/digest/auth_digest.cc' --- src/auth/digest/auth_digest.cc 2010-02-14 00:09:05 +0000 +++ src/auth/digest/auth_digest.cc 2010-03-02 16:58:13 +0000 @@ -67,6 +67,33 @@ CBDATA_TYPE(DigestAuthenticateStateData); +enum http_digest_attr_type { + DIGEST_USERNAME=1, + DIGEST_REALM, + DIGEST_QOP, + DIGEST_ALGORITHM, + DIGEST_URI, + DIGEST_NONCE, + DIGEST_NC, + DIGEST_CNONCE, + DIGEST_RESPONSE, + DIGEST_ENUM_END +}; + +static const HttpHeaderFieldAttrs DigestAttrs[DIGEST_ENUM_END] = { + {"username", (http_hdr_type)DIGEST_USERNAME}, + {"realm", (http_hdr_type)DIGEST_REALM}, + {"qop", (http_hdr_type)DIGEST_QOP}, + {"algorithm", (http_hdr_type)DIGEST_ALGORITHM}, + {"uri", (http_hdr_type)DIGEST_URI}, + {"nonce", (http_hdr_type)DIGEST_NONCE}, + {"nc", (http_hdr_type)DIGEST_NC}, + {"cnonce", (http_hdr_type)DIGEST_CNONCE}, + {"response", (http_hdr_type)DIGEST_RESPONSE}, +}; + +static HttpHeaderFieldInfo *DigestFieldsInfo = NULL; + /* * * Nonce Functions @@ -505,6 +532,9 @@ if (digestauthenticators) helperShutdown(digestauthenticators); + httpHeaderDestroyFieldsInfo(DigestFieldsInfo, DIGEST_ENUM_END); + DigestFieldsInfo = NULL; + authdigest_initialised = 0; if (!shutting_down) { @@ -867,6 +897,7 @@ AuthDigestConfig::init(AuthConfig * scheme) { if (authenticate) { + DigestFieldsInfo = httpHeaderBuildFieldsInfo(DigestAttrs, DIGEST_ENUM_END); authenticateDigestNonceSetup(); authdigest_initialised = 1; @@ -1090,124 +1121,84 @@ String temp(proxy_auth); while (strListGetItem(&temp, ',', &item, &ilen, &pos)) { - if ((p = strchr(item, '=')) && (p - item < ilen)) - ilen = p++ - item; - - if (!strncmp(item, "username", ilen)) { - /* white space */ - - while (xisspace(*p)) - p++; - - /* quote mark */ - p++; - + String value; + size_t nlen; + /* isolate directive name */ + if ((p = (const char *)memchr(item, '=', ilen)) && (p - item < ilen)) { + nlen = p++ - item; + if (!httpHeaderParseQuotedString(p, &value)) + value.limitInit(p, ilen - (p - item)); + } else + nlen = ilen; + + if (!value.defined()) { + debugs(29, 9, "authDigestDecodeAuth: Failed to parse attribute '" << temp << "' in '" << proxy_auth << "'"); + continue; + } + + /* find type */ + http_digest_attr_type type = (http_digest_attr_type)httpHeaderIdByName(item, nlen, DigestFieldsInfo, DIGEST_ENUM_END); + + switch (type) { + case DIGEST_USERNAME: safe_free(username); - username = xstrndup(p, strchr(p, '"') + 1 - p); - + username = xstrndup(value.rawBuf(), value.size() + 1); debugs(29, 9, "authDigestDecodeAuth: Found Username '" << username << "'"); - } else if (!strncmp(item, "realm", ilen)) { - /* white space */ - - while (xisspace(*p)) - p++; - - /* quote mark */ - p++; - + break; + + case DIGEST_REALM: safe_free(digest_request->realm); - digest_request->realm = xstrndup(p, strchr(p, '"') + 1 - p); - + digest_request->realm = xstrndup(value.rawBuf(), value.size() + 1); debugs(29, 9, "authDigestDecodeAuth: Found realm '" << digest_request->realm << "'"); - } else if (!strncmp(item, "qop", ilen)) { - /* white space */ - - while (xisspace(*p)) - p++; - - if (*p == '\"') - /* quote mark */ - p++; - + break; + + case DIGEST_QOP: safe_free(digest_request->qop); - digest_request->qop = xstrndup(p, strcspn(p, "\" \t\r\n()<>@,;:\\/[]?={}") + 1); - + digest_request->qop = xstrndup(value.rawBuf(), value.size() + 1); debugs(29, 9, "authDigestDecodeAuth: Found qop '" << digest_request->qop << "'"); - } else if (!strncmp(item, "algorithm", ilen)) { - /* white space */ - - while (xisspace(*p)) - p++; - - if (*p == '\"') - /* quote mark */ - p++; - + break; + + case DIGEST_ALGORITHM: safe_free(digest_request->algorithm); - digest_request->algorithm = xstrndup(p, strcspn(p, "\" \t\r\n()<>@,;:\\/[]?={}") + 1); - + digest_request->algorithm = xstrndup(value.rawBuf(), value.size() + 1); debugs(29, 9, "authDigestDecodeAuth: Found algorithm '" << digest_request->algorithm << "'"); - } else if (!strncmp(item, "uri", ilen)) { - /* white space */ - - while (xisspace(*p)) - p++; - - /* quote mark */ - p++; - + break; + + case DIGEST_URI: safe_free(digest_request->uri); - digest_request->uri = xstrndup(p, strchr(p, '"') + 1 - p); - + digest_request->uri = xstrndup(value.rawBuf(), value.size() + 1); debugs(29, 9, "authDigestDecodeAuth: Found uri '" << digest_request->uri << "'"); - } else if (!strncmp(item, "nonce", ilen)) { - /* white space */ - - while (xisspace(*p)) - p++; - - /* quote mark */ - p++; - + break; + + case DIGEST_NONCE: safe_free(digest_request->nonceb64); - digest_request->nonceb64 = xstrndup(p, strchr(p, '"') + 1 - p); - + digest_request->nonceb64 = xstrndup(value.rawBuf(), value.size() + 1); debugs(29, 9, "authDigestDecodeAuth: Found nonce '" << digest_request->nonceb64 << "'"); - } else if (!strncmp(item, "nc", ilen)) { - /* white space */ - - while (xisspace(*p)) - p++; - - xstrncpy(digest_request->nc, p, 9); - + break; + + case DIGEST_NC: + if (value.size() != 8) { + debugs(29, 9, "authDigestDecodeAuth: Invalid nc '" << value << "' in '" << temp << "'"); + } + xstrncpy(digest_request->nc, value.rawBuf(), value.size() + 1); debugs(29, 9, "authDigestDecodeAuth: Found noncecount '" << digest_request->nc << "'"); - } else if (!strncmp(item, "cnonce", ilen)) { - /* white space */ - - while (xisspace(*p)) - p++; - - /* quote mark */ - p++; - + break; + + case DIGEST_CNONCE: safe_free(digest_request->cnonce); - digest_request->cnonce = xstrndup(p, strchr(p, '"') + 1 - p); - + digest_request->cnonce = xstrndup(value.rawBuf(), value.size() + 1); debugs(29, 9, "authDigestDecodeAuth: Found cnonce '" << digest_request->cnonce << "'"); - } else if (!strncmp(item, "response", ilen)) { - /* white space */ - - while (xisspace(*p)) - p++; - - /* quote mark */ - p++; - + break; + + case DIGEST_RESPONSE: safe_free(digest_request->response); - digest_request->response = xstrndup(p, strchr(p, '"') + 1 - p); - + digest_request->response = xstrndup(value.rawBuf(), value.size() + 1); debugs(29, 9, "authDigestDecodeAuth: Found response '" << digest_request->response << "'"); + break; + + default: + debugs(29, 3, "authDigestDecodeAuth: Unknown attribute '" << item << "' in '" << temp << "'"); + } } @@ -1225,60 +1216,36 @@ * correct values - 400/401/407 */ - /* first the NONCE count */ - - if (digest_request->cnonce && strlen(digest_request->nc) != 8) { - debugs(29, 4, "authenticateDigestDecode: nonce count length invalid"); - return authDigestLogUsername(username, digest_request); - } - - /* now the nonce */ - nonce = authenticateDigestNonceFindNonce(digest_request->nonceb64); - - if (!nonce) { - /* we couldn't find a matching nonce! */ - debugs(29, 4, "authenticateDigestDecode: Unexpected or invalid nonce received"); - return authDigestLogUsername(username, digest_request); - } - - digest_request->nonce = nonce; - authDigestNonceLink(nonce); - - /* check the qop is what we expected. Note that for compatability with - * RFC 2069 we should support a missing qop. Tough. */ - - if (digest_request->qop && strcmp(digest_request->qop, QOP_AUTH) != 0) { - /* we received a qop option we didn't send */ - debugs(29, 4, "authenticateDigestDecode: Invalid qop option received"); + /* 2069 requirements */ + + /* do we have a username ? */ + if (!username || username[0] == '\0') { + debugs(29, 2, "authenticateDigestDecode: Empty or not present username"); + return authDigestLogUsername(username, digest_request); + } + + /* do we have a realm ? */ + if (!digest_request->realm || digest_request->realm[0] == '\0') { + debugs(29, 2, "authenticateDigestDecode: Empty or not present realm"); + return authDigestLogUsername(username, digest_request); + } + + /* and a nonce? */ + if (!digest_request->nonceb64 || digest_request->nonceb64[0] == '\0') { + debugs(29, 2, "authenticateDigestDecode: Empty or not present nonce"); return authDigestLogUsername(username, digest_request); } /* we can't check the URI just yet. We'll check it in the - * authenticate phase */ + * authenticate phase, but needs to be given */ + if (!digest_request->uri || digest_request->uri[0] == '\0') { + debugs(29, 2, "authenticateDigestDecode: Missing URI field"); + return authDigestLogUsername(username, digest_request); + } /* is the response the correct length? */ - if (!digest_request->response || strlen(digest_request->response) != 32) { - debugs(29, 4, "authenticateDigestDecode: Response length invalid"); - return authDigestLogUsername(username, digest_request); - } - - /* do we have a username ? */ - if (!username || username[0] == '\0') { - debugs(29, 4, "authenticateDigestDecode: Empty or not present username"); - return authDigestLogUsername(username, digest_request); - } - - /* check that we're not being hacked / the username hasn't changed */ - if (nonce->user && strcmp(username, nonce->user->username())) { - debugs(29, 4, "authenticateDigestDecode: Username for the nonce does not equal the username for the request"); - return authDigestLogUsername(username, digest_request); - } - - /* if we got a qop, did we get a cnonce or did we get a cnonce wihtout a qop? */ - if ((digest_request->qop && !digest_request->cnonce) - || (!digest_request->qop && digest_request->cnonce)) { - debugs(29, 4, "authenticateDigestDecode: qop without cnonce, or vice versa!"); + debugs(29, 2, "authenticateDigestDecode: Response length invalid"); return authDigestLogUsername(username, digest_request); } @@ -1291,6 +1258,54 @@ return authDigestLogUsername(username, digest_request); } + /* 2617 requirements, indicated by qop */ + if (digest_request->qop) { + + /* check the qop is what we expected. */ + if (strcmp(digest_request->qop, QOP_AUTH) != 0) { + /* we received a qop option we didn't send */ + debugs(29, 2, "authenticateDigestDecode: Invalid qop option received"); + return authDigestLogUsername(username, digest_request); + } + + /* check cnonce */ + if (!digest_request->cnonce || digest_request->cnonce[0] == '\0') { + debugs(29, 2, "authenticateDigestDecode: Missing URI field"); + return authDigestLogUsername(username, digest_request); + } + + /* check nc */ + if (strlen(digest_request->nc) != 8 || strspn(digest_request->nc, "0123456789abcdefABCDEF") != 8) { + debugs(29, 2, "authenticateDigestDecode: invalid nonce count"); + return authDigestLogUsername(username, digest_request); + } + } else { + /* cnonce and nc both require qop */ + if (digest_request->cnonce || digest_request->nc) { + debugs(29, 4, "authenticateDigestDecode: missing qop!"); + return authDigestLogUsername(username, digest_request); + } + } + + /** below nonce state dependent **/ + + /* now the nonce */ + nonce = authenticateDigestNonceFindNonce(digest_request->nonceb64); + if (!nonce) { + /* we couldn't find a matching nonce! */ + debugs(29, 2, "authenticateDigestDecode: Unexpected or invalid nonce received"); + return authDigestLogUsername(username, digest_request); + } + + digest_request->nonce = nonce; + authDigestNonceLink(nonce); + + /* check that we're not being hacked / the username hasn't changed */ + if (nonce->user && strcmp(username, nonce->user->username())) { + debugs(29, 2, "authenticateDigestDecode: Username for the nonce does not equal the username for the request"); + return authDigestLogUsername(username, digest_request); + } + /* the method we'll check at the authenticate step as well */ # Begin bundle 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
