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);