From 595b3830680595c9d375896e8b92f5356f558a53 Mon Sep 17 00:00:00 2001
From: Daniel Stenberg <daniel@haxx.se>
Date: Wed, 10 Jun 2026 10:27:50 +0200
Subject: [PATCH] digest: flush state on origin or credential change

Verified by test 1686

Closes #21944

(cherry picked from commit 5c6b4880357ab3e72967c1c45cae0f96ffabc535)
---
 lib/http_digest.c          |  34 +++++++++++++
 lib/urldata.h              |   7 +++
 lib/vauth/digest.c         |   3 ++
 lib/vauth/digest_sspi.c    |   5 ++
 tests/data/Makefile.am     |   2 +-
 tests/data/test1686        |  83 ++++++++++++++++++++++++++++++
 tests/libtest/Makefile.inc |   4 ++
 tests/libtest/lib1686.c    | 102 +++++++++++++++++++++++++++++++++++++
 8 files changed, 239 insertions(+), 1 deletion(-)
 create mode 100644 tests/data/test1686
 create mode 100644 tests/libtest/lib1686.c

diff --git a/lib/http_digest.c b/lib/http_digest.c
index 651bb83403..40137f4391 100644
--- a/lib/http_digest.c
+++ b/lib/http_digest.c
@@ -104,6 +104,36 @@ CURLcode Curl_output_digest(struct Curl_easy *data,
 #endif
   }
   else {
+    bool flush = FALSE;
+    const char *curorigin = data->conn ? data->conn->destination : NULL;
+    const char *curuser = data->state.aptr.user ?
+      data->state.aptr.user : "";
+    const char *curpasswd = data->state.aptr.passwd ?
+      data->state.aptr.passwd : "";
+
+    if(data->state.digest.origin && curorigin &&
+       strcmp(data->state.digest.origin, curorigin))
+      flush = TRUE;
+    else if(data->state.digest.creduser &&
+            (strcmp(data->state.digest.creduser, curuser) ||
+             strcmp(data->state.digest.credpasswd ?
+                    data->state.digest.credpasswd : "", curpasswd)))
+      flush = TRUE;
+
+    if(flush)
+      /* flush host Digest state */
+      Curl_auth_digest_cleanup(&data->state.digest);
+
+    /* remember the origin and credentials this host Digest state is for */
+    if(curorigin) {
+      Curl_safefree(data->state.digest.origin);
+      data->state.digest.origin = strdup(curorigin);
+    }
+    Curl_safefree(data->state.digest.creduser);
+    data->state.digest.creduser = strdup(curuser);
+    Curl_safefree(data->state.digest.credpasswd);
+    data->state.digest.credpasswd = strdup(curpasswd);
+
     digest = &data->state.digest;
     allocuserpwd = &data->state.aptr.userpwd;
     userp = data->state.aptr.user;
diff --git a/lib/urldata.h b/lib/urldata.h
index 45052e84b1..8655e8186a 100644
--- a/lib/urldata.h
+++ b/lib/urldata.h
@@ -318,6 +318,9 @@ struct ssl_general_config {
 #ifndef CURL_DISABLE_DIGEST_AUTH
 /* Struct used for Digest challenge-response authentication */
 struct digestdata {
+  char *origin;
+  char *creduser;
+  char *credpasswd;
 #if defined(USE_WINDOWS_SSPI)
   BYTE *input_token;
   size_t input_token_len;
diff --git a/lib/vauth/digest.c b/lib/vauth/digest.c
index ec4e82256c..225e993e72 100644
--- a/lib/vauth/digest.c
+++ b/lib/vauth/digest.c
@@ -998,6 +998,9 @@ CURLcode Curl_auth_create_digest_http_message(struct Curl_easy *data,
  */
 void Curl_auth_digest_cleanup(struct digestdata *digest)
 {
+  Curl_safefree(digest->origin);
+  Curl_safefree(digest->creduser);
+  Curl_safefree(digest->credpasswd);
   Curl_safefree(digest->nonce);
   Curl_safefree(digest->cnonce);
   Curl_safefree(digest->realm);
diff --git a/lib/vauth/digest_sspi.c b/lib/vauth/digest_sspi.c
index 2761c59965..428ffd6916 100644
--- a/lib/vauth/digest_sspi.c
+++ b/lib/vauth/digest_sspi.c
@@ -678,6 +678,11 @@ void Curl_auth_digest_cleanup(struct digestdata *digest)
   /* Free the copy of user/passwd used to make the identity for http_context */
   Curl_safefree(digest->user);
   Curl_safefree(digest->passwd);
+
+  /* Free the origin/credentials tracked for host Digest state flushing */
+  Curl_safefree(digest->origin);
+  Curl_safefree(digest->creduser);
+  Curl_safefree(digest->credpasswd);
 }
 
 #endif /* USE_WINDOWS_SSPI && !CURL_DISABLE_DIGEST_AUTH */
diff --git a/tests/data/Makefile.am b/tests/data/Makefile.am
index 1ef85cd3a2..b6a212763f 100644
--- a/tests/data/Makefile.am
+++ b/tests/data/Makefile.am
@@ -225,7 +225,7 @@ test1660 test1661 test1662 test1663 test1664 \
 \
 test1670 test1671 \
 \
-test1680 test1681 test1682 test1683 \
+test1680 test1681 test1682 test1683 test1686 \
 \
 test1700 test1701 test1702 test1703 test1704 test1705 test1706 test1707 \
 test1708 test1709 test1710 \
diff --git a/tests/data/test1686 b/tests/data/test1686
new file mode 100644
index 0000000000..a9413a6c4f
--- /dev/null
+++ b/tests/data/test1686
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="US-ASCII"?>
+<testcase>
+<info>
+<keywords>
+HTTP
+Digest
+</keywords>
+</info>
+
+<reply>
+<data crlf="headers" nocheck="yes">
+HTTP/1.1 401 Authorization Required
+Server: Apache/1.3.27 (Darwin) PHP/4.1.2
+WWW-Authenticate: Digest realm="my-backyard", nonce="314156295"
+Content-Length: 26
+
+This is not the real page
+</data>
+
+# This is supposed to be returned when the server gets a
+# Authorization: Digest line passed-in from the client
+<data1000 crlf="headers">
+HTTP/1.1 200 OK
+Server: Apache/1.3.27 (Darwin) PHP/4.1.2
+Content-Type: text/html; charset=iso-8859-1
+Content-Length: 23
+
+This IS the real page!
+</data1000>
+
+</reply>
+
+<client>
+<features>
+!SSPI
+crypto
+</features>
+<server>
+http
+</server>
+<name>
+HTTP Digest to different origins and switching credentials
+</name>
+<tool>
+lib%TESTNUMBER
+</tool>
+<command>
+%HOSTIP %HTTPPORT
+</command>
+</client>
+
+<verify>
+<protocol crlf="headers">
+GET /api HTTP/1.1
+Host: first.test:%HTTPPORT
+Accept: */*
+
+GET /api HTTP/1.1
+Host: first.test:%HTTPPORT
+Authorization: Digest username="alice", realm="my-backyard", nonce="314156295", uri="/api", response="4ecc00e567c37a9d537727890c2e5b32"
+Accept: */*
+
+GET /hook HTTP/1.1
+Host: second.test:%HTTPPORT
+Accept: */*
+
+GET /hook HTTP/1.1
+Host: second.test:%HTTPPORT
+Authorization: Digest username="alice", realm="my-backyard", nonce="314156295", uri="/hook", response="d3a7738fb6a23f5543fb8dacc0f0f253"
+Accept: */*
+
+GET /hook HTTP/1.1
+Host: second.test:%HTTPPORT
+Accept: */*
+
+GET /hook HTTP/1.1
+Host: second.test:%HTTPPORT
+Authorization: Digest username="bob", realm="my-backyard", nonce="314156295", uri="/hook", response="777e68eddb77294d9cbd6134973cbbab"
+Accept: */*
+
+</protocol>
+</verify>
+</testcase>
diff --git a/tests/libtest/Makefile.inc b/tests/libtest/Makefile.inc
index d130d5158d..1dcd51434f 100644
--- a/tests/libtest/Makefile.inc
+++ b/tests/libtest/Makefile.inc
@@ -68,6 +68,7 @@ LIBTESTPROGS = libauthretry libntlmconnect libprereq                     \
  \
  lib1647 \
  lib1662 \
+ lib1686 \
  \
  lib1900 lib1901 \
  lib1903 lib1905 lib1906 lib1907 lib1908 lib1910 lib1911 lib1912 lib1913 \
@@ -566,6 +568,8 @@ lib1598_LDADD = $(TESTUTIL_LIBS)
 lib1662_SOURCES = lib1662.c $(SUPPORTFILES) $(TESTUTIL) $(WARNLESS)
 lib1662_LDADD = $(TESTUTIL_LIBS)
 
+lib1686_SOURCES = lib1686.c $(SUPPORTFILES)
+
 lib1900_SOURCES = lib1900.c $(SUPPORTFILES)
 
 lib1901_SOURCES = lib1901.c $(SUPPORTFILES)
diff --git a/tests/libtest/lib1686.c b/tests/libtest/lib1686.c
new file mode 100644
index 0000000000..fd1ed4ed9c
--- /dev/null
+++ b/tests/libtest/lib1686.c
@@ -0,0 +1,98 @@
+/***************************************************************************
+ *                                  _   _ ____  _
+ *  Project                     ___| | | |  _ \| |
+ *                             / __| | | | |_) | |
+ *                            | (__| |_| |  _ <| |___
+ *                             \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+#include "test.h"
+
+#include "memdebug.h"
+
+static size_t devnull_1686(char *p, size_t s, size_t n, void *u)
+{
+  (void)p;
+  (void)u;
+  return s * n;
+}
+
+#define FIRSTHOST "first.test"
+#define SECONDHOST "second.test"
+
+CURLcode test(const char *hostip)
+{
+  CURL *curl = NULL;
+  CURLcode result = CURLE_OK;
+  const char *httpport = libtest_arg2;
+  char firsturl[100];
+  char secondurl[100];
+  char firstres[100];
+  char secondres[100];
+  struct curl_slist *host = NULL;
+  struct curl_slist *host2 = NULL;
+
+  if(curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) {
+    curl_mfprintf(stderr, "curl_global_init() failed\n");
+    return TEST_ERR_MAJOR_BAD;
+  }
+
+  /* create strings for CURLOPT_RESOLVE */
+  curl_msnprintf(firstres, sizeof(firstres), "%s:%s:%s",
+                 FIRSTHOST, httpport, hostip);
+  curl_msnprintf(secondres, sizeof(secondres), "%s:%s:%s",
+                 SECONDHOST, httpport, hostip);
+
+  /* create URLs */
+  curl_msnprintf(firsturl, sizeof(firsturl), "http://%s:%s/api",
+                 FIRSTHOST, httpport);
+  curl_msnprintf(secondurl, sizeof(secondurl), "http://%s:%s/hook",
+                 SECONDHOST, httpport);
+
+  host = curl_slist_append(NULL, firstres);
+  if(!host)
+    goto test_cleanup;
+  host2 = curl_slist_append(host, secondres);
+  if(!host2)
+    goto test_cleanup;
+  host = host2;
+
+  curl = curl_easy_init();
+  if(curl) {
+    easy_setopt(curl, CURLOPT_RESOLVE, host);
+    easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
+    easy_setopt(curl, CURLOPT_USERPWD, "alice:bond");
+    easy_setopt(curl, CURLOPT_WRITEFUNCTION, devnull_1686);
+
+    easy_setopt(curl, CURLOPT_URL, firsturl);
+    result = curl_easy_perform(curl);
+
+    easy_setopt(curl, CURLOPT_URL, secondurl);
+    result = curl_easy_perform(curl);
+
+    easy_setopt(curl, CURLOPT_USERPWD, "bob:secret");
+    easy_setopt(curl, CURLOPT_URL, secondurl);
+    result = curl_easy_perform(curl);
+  }
+
+test_cleanup:
+  curl_easy_cleanup(curl);
+  curl_global_cleanup();
+  curl_slist_free_all(host);
+  return result;
+}
-- 
2.54.0

