Commit 4d7195caf7 for openssl.org

commit 4d7195caf7af273fb56f3bf9b93ee89221615342
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date:   Sat Sep 5 11:13:30 2026 +0900

    http: reject HTTPS downgrades after relative redirects

    OSSL_HTTP_get() replaces current_url with a relative Location while
    reusing the request context. Checking the previous URL's scheme therefore
    allows a subsequent absolute HTTP redirect to bypass the downgrade guard.

    Pass the existing use_ssl flag to redirection_ok(). It survives relative
    redirects and is updated when an absolute URL is parsed, also covering
    HTTP-to-HTTPS upgrades followed by a relative redirect and an HTTP target.

    Add a parameterized memory BIO test covering both bypass cases, direct
    downgrade rejection, and allowed HTTP and HTTPS redirects. Document the
    existing restriction on redirects from HTTPS to HTTP.

    Fixes: #32688
    Assisted-by: Codex:gpt-6
    Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
    Reviewed-by: Dmitry Belyavskiy <beldmit@gmail.com>
    Reviewed-by: David von Oheimb <david.von.oheimb@siemens.com>
    Merge-date: Wed Sep 16 09:29:19 2026
    Merged-from: https://github.com/openssl/openssl/pull/32694

diff --git a/crypto/http/http_client.c b/crypto/http/http_client.c
index b9d2c51483..db0aaf6d8b 100644
--- a/crypto/http/http_client.c
+++ b/crypto/http/http_client.c
@@ -1264,7 +1264,7 @@ BIO *OSSL_HTTP_exchange(OSSL_HTTP_REQ_CTX *rctx, char **redirection_url)
     return resp;
 }

-static int redirection_ok(int n_redir, const char *old_url, const char *new_url)
+static int redirection_ok(int n_redir, int use_ssl, const char *new_url)
 {
     if (n_redir >= HTTP_VERSION_MAX_REDIRECTIONS) {
         ERR_raise(ERR_LIB_HTTP, HTTP_R_TOO_MANY_REDIRECTIONS);
@@ -1272,7 +1272,7 @@ static int redirection_ok(int n_redir, const char *old_url, const char *new_url)
     }
     if (*new_url == '/') /* redirection to same server => same protocol */
         return 1;
-    if (HAS_PREFIX(old_url, OSSL_HTTPS_NAME ":") && !HAS_PREFIX(new_url, OSSL_HTTPS_NAME ":")) {
+    if (use_ssl && !HAS_PREFIX(new_url, OSSL_HTTPS_NAME ":")) {
         ERR_raise(ERR_LIB_HTTP, HTTP_R_REDIRECTION_FROM_HTTPS_TO_HTTP);
         return 0;
     }
@@ -1331,7 +1331,7 @@ BIO *OSSL_HTTP_get(const char *url, const char *proxy, const char *no_proxy,
         }
         OPENSSL_free(path);
         if (resp == NULL && redirection_url != NULL) {
-            if (redirection_ok(++n_redirs, current_url, redirection_url)
+            if (redirection_ok(++n_redirs, use_ssl, redirection_url)
                 && may_still_retry(max_time, &timeout)) {
                 (void)BIO_reset(bio);
                 OPENSSL_free(current_url);
diff --git a/doc/man3/OSSL_HTTP_transfer.pod b/doc/man3/OSSL_HTTP_transfer.pod
index b854756ffd..573df2b4a9 100644
--- a/doc/man3/OSSL_HTTP_transfer.pod
+++ b/doc/man3/OSSL_HTTP_transfer.pod
@@ -207,7 +207,8 @@ The caller is responsible for freeing the BIO pointer obtained.

 OSSL_HTTP_get() uses HTTP GET to obtain data from I<bio> if non-NULL,
 else from the server contained in the I<url>, and returns it as a BIO.
-It supports redirection via HTTP status code 301 or 302.  It is meant for
+It supports redirection via HTTP status code 301 or 302.
+Redirection from HTTPS to HTTP is not allowed.  It is meant for
 transfers with a single round trip, so does not support persistent connections.
 If I<bio> is non-NULL, any host and port components in the I<url> are not used
 for connecting but the hostname is used, as usual, for the C<Host> header.
diff --git a/test/http_test.c b/test/http_test.c
index fc2c6331d1..70921f5d8b 100644
--- a/test/http_test.c
+++ b/test/http_test.c
@@ -9,6 +9,7 @@
  */

 #include <openssl/http.h>
+#include <openssl/httperr.h>
 #include <openssl/pem.h>
 #include <openssl/x509v3.h>
 #include <openssl/err.h>
@@ -240,6 +241,82 @@ err:
     return res;
 }

+static const struct {
+    const char *url;
+    const char *redirects[4];
+    int success;
+} redirect_tests[] = {
+    { "https://server/start", { "http://server/end" }, 0 },
+    { "https://server/start", { "/relative", "http://server/end" }, 0 },
+    { "http://server/start",
+        { "https://server/secure", "/relative", "http://server/end" }, 0 },
+    { "http://server/start", { "/relative", "http://server/end" }, 1 },
+    { "https://server/start", { "/relative", "https://server/end" }, 1 },
+};
+
+/* Replace each flushed request with the next response, using a single mem BIO. */
+static long http_redirect_cb(BIO *bio, int oper, const char *argp, size_t len,
+    int cmd, long argl, int ret, size_t *processed)
+{
+    const char *const *redirect = (const char *const *)BIO_get_callback_arg(bio);
+
+    if (oper != (BIO_CB_CTRL | BIO_CB_RETURN))
+        return ret;
+    if (cmd == BIO_C_DO_STATE_MACHINE)
+        return 1; /* mock a successful connection */
+    if (cmd != BIO_CTRL_FLUSH)
+        return ret;
+    if (!TEST_int_eq(BIO_reset(bio), 1))
+        return 0;
+    if (*redirect != NULL) {
+        BIO_set_callback_arg(bio, (char *)(redirect + 1));
+        return BIO_printf(bio, "HTTP/1.0 302 Found\r\nLocation: %s\r\n\r\n",
+                   *redirect)
+            > 0;
+    }
+    return BIO_puts(bio, "HTTP/1.0 200 OK\r\nContent-Length: 5\r\n\r\n" text1) > 0;
+}
+
+/* The redirect policy uses the requested protocol; no actual TLS is needed. */
+static BIO *http_noop_update(BIO *bio, void *arg, int connect, int detail)
+{
+    return bio;
+}
+
+static int test_http_redirect(int idx)
+{
+    BIO *bio = BIO_new(BIO_s_mem());
+    BIO *rsp = NULL;
+    char buf[sizeof(text1)];
+    unsigned long err;
+    int res = 0;
+
+    if (!TEST_ptr(bio))
+        goto end;
+    BIO_set_callback_ex(bio, http_redirect_cb);
+    BIO_set_callback_arg(bio, (char *)redirect_tests[idx].redirects);
+    ERR_clear_error();
+    rsp = OSSL_HTTP_get(redirect_tests[idx].url, NULL, NULL, bio, NULL,
+        http_noop_update, NULL, 0, NULL, NULL, 0,
+        OSSL_HTTP_DEFAULT_MAX_RESP_LEN, 0);
+    if (redirect_tests[idx].success) {
+        res = TEST_ptr(rsp)
+            && TEST_int_eq(BIO_read(rsp, buf, sizeof(buf)), sizeof(text1) - 1)
+            && TEST_mem_eq(buf, sizeof(text1) - 1, text1, sizeof(text1) - 1);
+    } else {
+        err = ERR_peek_last_error();
+        res = TEST_ptr_null(rsp)
+            && TEST_int_eq(ERR_GET_LIB(err), ERR_LIB_HTTP)
+            && TEST_int_eq(ERR_GET_REASON(err), HTTP_R_REDIRECTION_FROM_HTTPS_TO_HTTP);
+    }
+
+end:
+    BIO_free(rsp);
+    BIO_free(bio);
+    ERR_clear_error();
+    return res;
+}
+
 static int test_http_keep_alive(char version, int keep_alive, int kept_alive)
 {
     BIO *wbio = BIO_new(BIO_s_mem());
@@ -682,6 +759,7 @@ int setup_tests(void)

     ADD_TEST(test_http_get_txt);
     ADD_TEST(test_http_get_txt_redirected);
+    ADD_ALL_TESTS(test_http_redirect, OSSL_NELEM(redirect_tests));
     ADD_TEST(test_http_get_txt_fatal_status);
     ADD_TEST(test_http_get_txt_error_status);
     ADD_TEST(test_http_post_txt);