diff --git a/httpd-2.4.63-CVE-2024-42516.patch b/httpd-2.4.63-CVE-2024-42516.patch new file mode 100644 index 0000000..19891cc --- /dev/null +++ b/httpd-2.4.63-CVE-2024-42516.patch @@ -0,0 +1,303 @@ +From a7a9d814c7c23e990283277230ddd5a9efec27c7 Mon Sep 17 00:00:00 2001 +From: Eric Covener +Date: Mon, 7 Jul 2025 11:59:38 +0000 +Subject: [PATCH] fix header merging + +Reviewed By: rpluem, jorton, ylavic + + + +git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.x@1927039 13f79535-47bb-0310-9956-ffa450edef68 +--- + modules/http/http_filters.c | 248 +++++++++++++++++++----------------- + 1 file changed, 128 insertions(+), 120 deletions(-) + +diff --git a/modules/http/http_filters.c b/modules/http/http_filters.c +index 60b44d78580..732fb8eb6a7 100644 +--- a/modules/http/http_filters.c ++++ b/modules/http/http_filters.c +@@ -1300,107 +1300,10 @@ typedef struct header_filter_ctx { + int headers_sent; + } header_filter_ctx; + +-AP_CORE_DECLARE_NONSTD(apr_status_t) ap_http_header_filter(ap_filter_t *f, +- apr_bucket_brigade *b) ++static void merge_response_headers(request_rec *r, const char **protocol) + { +- request_rec *r = f->r; +- conn_rec *c = r->connection; +- const char *clheader; +- int header_only = (r->header_only || AP_STATUS_IS_HEADER_ONLY(r->status)); +- const char *protocol = NULL; +- apr_bucket *e; +- apr_bucket_brigade *b2; +- header_struct h; +- header_filter_ctx *ctx = f->ctx; +- const char *ctype; +- ap_bucket_error *eb = NULL; +- apr_status_t rv = APR_SUCCESS; +- int recursive_error = 0; +- +- AP_DEBUG_ASSERT(!r->main); +- +- if (!ctx) { +- ctx = f->ctx = apr_pcalloc(r->pool, sizeof(header_filter_ctx)); +- } +- else if (ctx->headers_sent) { +- /* Eat body if response must not have one. */ +- if (header_only) { +- /* Still next filters may be waiting for EOS, so pass it (alone) +- * when encountered and be done with this filter. +- */ +- e = APR_BRIGADE_LAST(b); +- if (e != APR_BRIGADE_SENTINEL(b) && APR_BUCKET_IS_EOS(e)) { +- APR_BUCKET_REMOVE(e); +- apr_brigade_cleanup(b); +- APR_BRIGADE_INSERT_HEAD(b, e); +- ap_remove_output_filter(f); +- rv = ap_pass_brigade(f->next, b); +- } +- apr_brigade_cleanup(b); +- return rv; +- } +- } +- +- for (e = APR_BRIGADE_FIRST(b); +- e != APR_BRIGADE_SENTINEL(b); +- e = APR_BUCKET_NEXT(e)) +- { +- if (AP_BUCKET_IS_ERROR(e) && !eb) { +- eb = e->data; +- continue; +- } +- /* +- * If we see an EOC bucket it is a signal that we should get out +- * of the way doing nothing. +- */ +- if (AP_BUCKET_IS_EOC(e)) { +- ap_remove_output_filter(f); +- return ap_pass_brigade(f->next, b); +- } +- } +- +- if (!ctx->headers_sent && !check_headers(r)) { +- /* We may come back here from ap_die() below, +- * so clear anything from this response. +- */ +- apr_table_clear(r->headers_out); +- apr_table_clear(r->err_headers_out); +- r->content_type = r->content_encoding = NULL; +- r->content_languages = NULL; +- r->clength = r->chunked = 0; +- apr_brigade_cleanup(b); +- +- /* Don't recall ap_die() if we come back here (from its own internal +- * redirect or error response), otherwise we can end up in infinite +- * recursion; better fall through with 500, minimal headers and an +- * empty body (EOS only). +- */ +- if (!check_headers_recursion(r)) { +- ap_die(HTTP_INTERNAL_SERVER_ERROR, r); +- return AP_FILTER_ERROR; +- } +- r->status = HTTP_INTERNAL_SERVER_ERROR; +- e = ap_bucket_eoc_create(c->bucket_alloc); +- APR_BRIGADE_INSERT_TAIL(b, e); +- e = apr_bucket_eos_create(c->bucket_alloc); +- APR_BRIGADE_INSERT_TAIL(b, e); +- ap_set_content_length(r, 0); +- recursive_error = 1; +- } +- else if (eb) { +- int status; +- status = eb->status; +- apr_brigade_cleanup(b); +- ap_die(status, r); +- return AP_FILTER_ERROR; +- } +- +- if (r->assbackwards) { +- r->sent_bodyct = 1; +- ap_remove_output_filter(f); +- rv = ap_pass_brigade(f->next, b); +- goto out; +- } ++ const char *ctype = NULL; ++ const char *clheader = NULL; + + /* + * Now that we are ready to send a response, we need to combine the two +@@ -1430,6 +1333,9 @@ AP_CORE_DECLARE_NONSTD(apr_status_t) ap_http_header_filter(ap_filter_t *f, + fixup_vary(r); + } + ++ /* determine the protocol and whether we should use keepalives. */ ++ basic_http_header_check(r, protocol); ++ ap_set_keepalive(r); + + /* + * Control cachability for non-cacheable responses if not already set by +@@ -1449,10 +1355,6 @@ AP_CORE_DECLARE_NONSTD(apr_status_t) ap_http_header_filter(ap_filter_t *f, + apr_table_unset(r->headers_out, "ETag"); + } + +- /* determine the protocol and whether we should use keepalives. */ +- basic_http_header_check(r, &protocol); +- ap_set_keepalive(r); +- + /* 204/304 responses don't have content related headers */ + if (AP_STATUS_IS_HEADER_ONLY(r->status)) { + apr_table_unset(r->headers_out, "Transfer-Encoding"); +@@ -1513,30 +1415,136 @@ AP_CORE_DECLARE_NONSTD(apr_status_t) ap_http_header_filter(ap_filter_t *f, + && !strcmp(clheader, "0")) { + apr_table_unset(r->headers_out, "Content-Length"); + } ++} + +- b2 = apr_brigade_create(r->pool, c->bucket_alloc); +- basic_http_header(r, b2, protocol); +- +- h.pool = r->pool; +- h.bb = b2; ++AP_CORE_DECLARE_NONSTD(apr_status_t) ap_http_header_filter(ap_filter_t *f, ++ apr_bucket_brigade *b) ++{ ++ request_rec *r = f->r; ++ conn_rec *c = r->connection; ++ int header_only = (r->header_only || AP_STATUS_IS_HEADER_ONLY(r->status)); ++ apr_bucket *e; ++ apr_bucket_brigade *b2; ++ header_struct h; ++ header_filter_ctx *ctx = f->ctx; ++ ap_bucket_error *eb = NULL; ++ apr_status_t rv = APR_SUCCESS; ++ int recursive_error = 0; ++ const char *protocol; + +- send_all_header_fields(&h, r); ++ AP_DEBUG_ASSERT(!r->main); + +- terminate_header(b2); ++ if (!ctx) { ++ ctx = f->ctx = apr_pcalloc(r->pool, sizeof(header_filter_ctx)); ++ } ++ else if (ctx->headers_sent) { ++ /* Eat body if response must not have one. */ ++ if (header_only) { ++ /* Still next filters may be waiting for EOS, so pass it (alone) ++ * when encountered and be done with this filter. ++ */ ++ e = APR_BRIGADE_LAST(b); ++ if (e != APR_BRIGADE_SENTINEL(b) && APR_BUCKET_IS_EOS(e)) { ++ APR_BUCKET_REMOVE(e); ++ apr_brigade_cleanup(b); ++ APR_BRIGADE_INSERT_HEAD(b, e); ++ ap_remove_output_filter(f); ++ rv = ap_pass_brigade(f->next, b); ++ } ++ apr_brigade_cleanup(b); ++ return rv; ++ } ++ } + +- if (header_only) { +- e = APR_BRIGADE_LAST(b); +- if (e != APR_BRIGADE_SENTINEL(b) && APR_BUCKET_IS_EOS(e)) { +- APR_BUCKET_REMOVE(e); +- APR_BRIGADE_INSERT_TAIL(b2, e); ++ for (e = APR_BRIGADE_FIRST(b); ++ e != APR_BRIGADE_SENTINEL(b); ++ e = APR_BUCKET_NEXT(e)) ++ { ++ if (AP_BUCKET_IS_ERROR(e) && !eb) { ++ eb = e->data; ++ continue; ++ } ++ /* ++ * If we see an EOC bucket it is a signal that we should get out ++ * of the way doing nothing. ++ */ ++ if (AP_BUCKET_IS_EOC(e)) { + ap_remove_output_filter(f); ++ return ap_pass_brigade(f->next, b); ++ } ++ } ++ ++ if (!ctx->headers_sent) { ++ merge_response_headers(r, &protocol); ++ if (!check_headers(r)) { ++ /* We may come back here from ap_die() below, ++ * so clear anything from this response. ++ */ ++ apr_table_clear(r->headers_out); ++ apr_table_clear(r->err_headers_out); ++ r->content_type = r->content_encoding = NULL; ++ r->content_languages = NULL; ++ r->clength = r->chunked = 0; ++ apr_brigade_cleanup(b); ++ ++ /* Don't recall ap_die() if we come back here (from its own internal ++ * redirect or error response), otherwise we can end up in infinite ++ * recursion; better fall through with 500, minimal headers and an ++ * empty body (EOS only). ++ */ ++ if (!check_headers_recursion(r)) { ++ ap_die(HTTP_INTERNAL_SERVER_ERROR, r); ++ return AP_FILTER_ERROR; ++ } ++ r->status = HTTP_INTERNAL_SERVER_ERROR; ++ e = ap_bucket_eoc_create(c->bucket_alloc); ++ APR_BRIGADE_INSERT_TAIL(b, e); ++ e = apr_bucket_eos_create(c->bucket_alloc); ++ APR_BRIGADE_INSERT_TAIL(b, e); ++ ap_set_content_length(r, 0); ++ recursive_error = 1; ++ } ++ else if (eb) { ++ int status; ++ status = eb->status; ++ apr_brigade_cleanup(b); ++ ap_die(status, r); ++ return AP_FILTER_ERROR; + } +- apr_brigade_cleanup(b); + } + +- rv = ap_pass_brigade(f->next, b2); +- apr_brigade_cleanup(b2); +- ctx->headers_sent = 1; ++ if (r->assbackwards) { ++ r->sent_bodyct = 1; ++ ap_remove_output_filter(f); ++ rv = ap_pass_brigade(f->next, b); ++ goto out; ++ } ++ ++ if (!ctx->headers_sent) { ++ b2 = apr_brigade_create(r->pool, c->bucket_alloc); ++ basic_http_header(r, b2, protocol); ++ ++ h.pool = r->pool; ++ h.bb = b2; ++ ++ send_all_header_fields(&h, r); ++ ++ terminate_header(b2); ++ ++ if (header_only) { ++ e = APR_BRIGADE_LAST(b); ++ if (e != APR_BRIGADE_SENTINEL(b) && APR_BUCKET_IS_EOS(e)) { ++ APR_BUCKET_REMOVE(e); ++ APR_BRIGADE_INSERT_TAIL(b2, e); ++ ap_remove_output_filter(f); ++ } ++ apr_brigade_cleanup(b); ++ } ++ ++ rv = ap_pass_brigade(f->next, b2); ++ apr_brigade_cleanup(b2); ++ ctx->headers_sent = 1; ++ } + + if (rv != APR_SUCCESS || header_only) { + goto out; diff --git a/httpd-2.4.63-CVE-2026-24072.patch b/httpd-2.4.63-CVE-2026-24072.patch new file mode 100644 index 0000000..2af4ccb --- /dev/null +++ b/httpd-2.4.63-CVE-2026-24072.patch @@ -0,0 +1,80 @@ +diff --git a/modules/mappers/mod_rewrite.c b/modules/mappers/mod_rewrite.c +index 93430e5..c718f6f 100644 +--- a/modules/mappers/mod_rewrite.c ++++ b/modules/mappers/mod_rewrite.c +@@ -3673,12 +3673,17 @@ static const char *cmd_rewritecond(cmd_parms *cmd, void *in_dconf, + newcond->regexp = regexp; + } + else if (newcond->ptype == CONDPAT_AP_EXPR) { ++ int in_htaccess = cmd->pool == cmd->temp_pool; + unsigned int flags = newcond->flags & CONDFLAG_NOVARY ? + AP_EXPR_FLAG_DONT_VARY : 0; ++ /* Use restricted ap_expr() parser in htaccess context. */ ++ if (in_htaccess) flags |= AP_EXPR_FLAG_RESTRICTED; + newcond->expr = ap_expr_parse_cmd(cmd, a2, flags, &err, NULL); + if (err) + return apr_psprintf(cmd->pool, "RewriteCond: cannot compile " +- "expression \"%s\": %s", a2, err); ++ "expression%s \"%s\" %s", ++ in_htaccess ? " in htaccess context" : "", ++ a2, err); + } + + return NULL; +diff --git a/modules/metadata/mod_setenvif.c b/modules/metadata/mod_setenvif.c +index 23d60cd..b74c9c0 100644 +--- a/modules/metadata/mod_setenvif.c ++++ b/modules/metadata/mod_setenvif.c +@@ -422,6 +422,12 @@ static const char *add_setenvifexpr(cmd_parms *cmd, void *mconfig, + sei_cfg_rec *sconf; + sei_entry *new; + const char *err; ++ unsigned int flags = 0; ++ ++ /* Use restricted ap_expr() parser in htaccess context. */ ++ if (cmd->pool == cmd->temp_pool) { ++ flags |= AP_EXPR_FLAG_RESTRICTED; ++ } + + /* + * Determine from our context into which record to put the entry. +@@ -445,7 +451,7 @@ static const char *add_setenvifexpr(cmd_parms *cmd, void *mconfig, + new->regex = NULL; + new->pattern = NULL; + new->preg = NULL; +- new->expr = ap_expr_parse_cmd(cmd, expr, 0, &err, NULL); ++ new->expr = ap_expr_parse_cmd(cmd, expr, flags, &err, NULL); + if (err) + return apr_psprintf(cmd->pool, "Could not parse expression \"%s\": %s", + expr, err); +diff --git a/modules/proxy/mod_proxy_fcgi.c b/modules/proxy/mod_proxy_fcgi.c +index 128cf1e..ef090dd 100644 +--- a/modules/proxy/mod_proxy_fcgi.c ++++ b/modules/proxy/mod_proxy_fcgi.c +@@ -1338,9 +1338,15 @@ static const char *cmd_setenv(cmd_parms *cmd, void *in_dconf, + const char *err; + sei_entry *new; + const char *envvar = arg2; ++ unsigned int flags = 0; ++ ++ /* Use restricted ap_expr() parser in htaccess context. */ ++ if (cmd->pool == cmd->temp_pool) { ++ flags |= AP_EXPR_FLAG_RESTRICTED; ++ } + + new = apr_array_push(dconf->env_fixups); +- new->cond = ap_expr_parse_cmd(cmd, arg1, 0, &err, NULL); ++ new->cond = ap_expr_parse_cmd(cmd, arg1, flags, &err, NULL); + if (err) { + return apr_psprintf(cmd->pool, "Could not parse expression \"%s\": %s", + arg1, err); +@@ -1367,7 +1373,8 @@ static const char *cmd_setenv(cmd_parms *cmd, void *in_dconf, + arg3 = ""; + } + +- new->subst = ap_expr_parse_cmd(cmd, arg3, AP_EXPR_FLAG_STRING_RESULT, &err, NULL); ++ flags |= AP_EXPR_FLAG_STRING_RESULT; ++ new->subst = ap_expr_parse_cmd(cmd, arg3, flags, &err, NULL); + if (err) { + return apr_psprintf(cmd->pool, "Could not parse expression \"%s\": %s", + arg3, err); diff --git a/httpd-2.4.63-CVE-2026-29169.patch b/httpd-2.4.63-CVE-2026-29169.patch new file mode 100644 index 0000000..38bf07f --- /dev/null +++ b/httpd-2.4.63-CVE-2026-29169.patch @@ -0,0 +1,38 @@ +From 225dc070adba11040b774cf641e1d8bc79941643 Mon Sep 17 00:00:00 2001 +From: Eric Covener +Date: Sun, 26 Apr 2026 16:04:35 +0000 +Subject: [PATCH] Merge r1933353 from trunk: + +mod_dav_lock: use the right dav_lock_discovery + + + +git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.x@1933354 13f79535-47bb-0310-9956-ffa450edef68 +--- + modules/dav/lock/locks.c | 12 ++++++------ + 1 file changed, 6 insertions(+), 6 deletions(-) + +diff --git a/modules/dav/lock/locks.c b/modules/dav/lock/locks.c +index 0f072ec0647..d357e4572d1 100644 +--- a/modules/dav/lock/locks.c ++++ b/modules/dav/lock/locks.c +@@ -1190,13 +1190,13 @@ static dav_error * dav_generic_refresh_locks(dav_lockdb *lockdb, + } + if (dav_generic_do_refresh(dp_scan, ltl, new_time)) { + /* the lock was refreshed. return the lock. */ +- newlock = dav_generic_alloc_lock(lockdb, ip->key, dp->locktoken); ++ newlock = dav_generic_alloc_lock(lockdb, ip->key, dp_scan->locktoken); + newlock->is_locknull = !resource->exists; +- newlock->scope = dp->f.scope; +- newlock->type = dp->f.type; +- newlock->depth = dp->f.depth; +- newlock->timeout = dp->f.timeout; +- newlock->owner = dp->owner; ++ newlock->scope = dp_scan->f.scope; ++ newlock->type = dp_scan->f.type; ++ newlock->depth = dp_scan->f.depth; ++ newlock->timeout = dp_scan->f.timeout; ++ newlock->owner = dp_scan->owner; + newlock->auth_user = dp_scan->auth_user; + + newlock->next = *locks; diff --git a/httpd-2.4.63-CVE-2026-33006.patch b/httpd-2.4.63-CVE-2026-33006.patch new file mode 100644 index 0000000..72a2bd1 --- /dev/null +++ b/httpd-2.4.63-CVE-2026-33006.patch @@ -0,0 +1,349 @@ +diff --git a/include/httpd.h b/include/httpd.h +index 6c7484f..5a3cce4 100644 +--- a/include/httpd.h ++++ b/include/httpd.h +@@ -2125,6 +2125,54 @@ AP_DECLARE(int) ap_ind(const char *str, char c); /* Sigh... */ + */ + AP_DECLARE(int) ap_rind(const char *str, char c); + ++/** ++ * Check whether two buffers of equal size have the same content, using a ++ * constant time algorithm (branch-less with regard to the content of the ++ * buffers and an execution time solely dependent on the number of bytes ++ * compared, not the bytes themselves). ++ * ++ * @param buf1 first buffer to compare ++ * @param buf2 second buffer to compare ++ * @param n number of bytes to compare ++ * @return 1 if equal, 0 otherwise ++ */ ++AP_DECLARE(int) ap_memeq_timingsafe(const void *buf1, const void *buf2, ++ apr_size_t n); ++ ++/** ++ * Check whether two NUL-terminated strings have the same content, using a ++ * constant time algorithm (branch-less with regard to the content of the ++ * secret string and an execution time solely dependent on the length of ++ * the non-secret string). The secret string of the two should be set in ++ * the first parameter \c sec1 to avoid leaking its length. ++ * ++ * @param sec1 first string to compare (the secret one) ++ * @param str2 second string to compare ++ * @return 1 if equal, 0 otherwise ++ * @remark The function will compare as much characters as there are in ++ * \c str2, so the length of \c str2 might leak through side channel, ++ * while the length of \c sec1 does not. ++ */ ++AP_DECLARE(int) ap_streq_timingsafe(const char *sec1, const char *str2); ++ ++/** ++ * Check whether two NUL-terminated strings have the same content, up to \c n ++ * characters, using a constant time algorithm (branch-less with regard to the ++ * content of the secret string and an execution time solely dependent on the ++ * length of the non-secret string or \c n). The secret string of the two ++ * should be set in the first parameter \c sec1 to avoid leaking its length. ++ * ++ * @param sec1 secret string to compare ++ * @param str2 string to compare with ++ * @param n max number of characters to compare ++ * @return 1 if equal, 0 otherwise ++ * @remark The function will compare as much characters as there are in ++ * \c str2 if it's less than \c n, so the length of \c str2 might ++ * leak through side channel, while the length of \c sec1 does not. ++ */ ++AP_DECLARE(int) ap_strneq_timingsafe(const char *sec1, const char *str2, ++ apr_size_t n); ++ + /** + * Given a string, replace any bare " with \\" . + * @param p The pool to allocate memory from +diff --git a/modules/aaa/mod_auth_digest.c b/modules/aaa/mod_auth_digest.c +index 791cec2..f10af0d 100644 +--- a/modules/aaa/mod_auth_digest.c ++++ b/modules/aaa/mod_auth_digest.c +@@ -100,10 +100,17 @@ typedef struct digest_config_struct { + #define DFLT_NONCE_LIFE apr_time_from_sec(300) + #define NEXTNONCE_DELTA apr_time_from_sec(30) + +- ++/* The server nonce has fixed length and is the concatenation of: ++ * base64(apr_time_t timestamp) + hex(SHA1(realm+time[+opaque])) */ + #define NONCE_TIME_LEN (((sizeof(apr_time_t)+2)/3)*4) + #define NONCE_HASH_LEN (2*APR_SHA1_DIGESTSIZE) + #define NONCE_LEN (int )(NONCE_TIME_LEN + NONCE_HASH_LEN) ++/* Evaluates to true if nonce string is valid. Since the time part of ++ * the nonce is a base64 encoding of an apr_time_t (8 bytes), it ++ * must end with a '='. */ ++#define VALID_NONCE(n_) ((n_) && strlen((n_)) == NONCE_LEN && (n_)[NONCE_TIME_LEN - 1] == '=') ++ ++#define MD5_DIGEST_LEN (2*APR_MD5_DIGESTSIZE) /* ignoring trailing \0 */ + + #define SECRET_LEN 20 + #define RETAINED_DATA_ID "mod_auth_digest" +@@ -1017,8 +1024,9 @@ static int get_digest_rec(request_rec *r, digest_header_rec *resp) + resp->nonce_count = apr_pstrdup(r->pool, value); + } + +- if (!resp->username || !resp->realm || !resp->nonce || !resp->uri +- || !resp->digest ++ if (!resp->username || !resp->realm || !resp->uri ++ || !VALID_NONCE(resp->nonce) ++ || !resp->digest || strlen(resp->digest) != MD5_DIGEST_LEN + || (resp->message_qop && (!resp->cnonce || !resp->nonce_count))) { + resp->auth_hdr_sts = INVALID; + return !OK; +@@ -1070,14 +1078,9 @@ static int parse_hdr_and_update_nc(request_rec *r) + } + + +-/* +- * Nonce generation code +- */ +- +-/* The hash part of the nonce is a SHA-1 hash of the time, realm, server host +- * and port, opaque, and our secret. +- */ +-static void gen_nonce_hash(char *hash, const char *timestr, const char *opaque, ++/* Writes the hash part of the server nonce to hash, which must be of ++ * minimum size (NONCE_HASH_LEN+1). */ ++static void gen_nonce_hash(char hash[NONCE_HASH_LEN+1], const char *timestr, const char *opaque, + const server_rec *server, + const digest_config_rec *conf) + { +@@ -1426,19 +1429,6 @@ static int check_nonce(request_rec *r, digest_header_rec *resp, + time_rec nonce_time; + char tmp, hash[NONCE_HASH_LEN+1]; + +- /* Since the time part of the nonce is a base64 encoding of an +- * apr_time_t (8 bytes), it should end with a '=', fail early otherwise. +- */ +- if (strlen(resp->nonce) != NONCE_LEN +- || resp->nonce[NONCE_TIME_LEN - 1] != '=') { +- ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01775) +- "invalid nonce '%s' received - length is not %d " +- "or time encoding is incorrect", +- resp->nonce, NONCE_LEN); +- note_digest_auth_failure(r, conf, resp, 1); +- return HTTP_UNAUTHORIZED; +- } +- + tmp = resp->nonce[NONCE_TIME_LEN]; + resp->nonce[NONCE_TIME_LEN] = '\0'; + apr_base64_decode_binary(nonce_time.arr, resp->nonce); +@@ -1446,7 +1436,7 @@ static int check_nonce(request_rec *r, digest_header_rec *resp, + resp->nonce[NONCE_TIME_LEN] = tmp; + resp->nonce_time = nonce_time.time; + +- if (strcmp(hash, resp->nonce+NONCE_TIME_LEN)) { ++ if (!ap_memeq_timingsafe(hash, resp->nonce+NONCE_TIME_LEN, NONCE_HASH_LEN)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01776) + "invalid nonce %s received - hash is not %s", + resp->nonce, hash); +@@ -1796,7 +1786,7 @@ static int authenticate_digest_user(request_rec *r) + + if (resp->message_qop == NULL) { + /* old (rfc-2069) style digest */ +- if (strcmp(resp->digest, old_digest(r, resp))) { ++ if (!ap_memeq_timingsafe(old_digest(r, resp), resp->digest, MD5_DIGEST_LEN)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01792) + "user %s: password mismatch: %s", r->user, + r->uri); +@@ -1831,7 +1821,7 @@ static int authenticate_digest_user(request_rec *r) + /* we failed to allocate a client struct */ + return HTTP_INTERNAL_SERVER_ERROR; + } +- if (strcmp(resp->digest, exp_digest)) { ++ if (!ap_memeq_timingsafe(exp_digest, resp->digest, MD5_DIGEST_LEN)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01794) + "user %s: password mismatch: %s", r->user, + r->uri); +diff --git a/modules/session/mod_session_crypto.c b/modules/session/mod_session_crypto.c +index fe39f2c..b33847b 100644 +--- a/modules/session/mod_session_crypto.c ++++ b/modules/session/mod_session_crypto.c +@@ -69,8 +69,6 @@ typedef struct { + #define AP_SIPHASH_KSIZE APR_SIPHASH_KSIZE + #define ap_siphash24_auth apr_siphash24_auth + +-#define ap_crypto_equals apr_crypto_equals +- + #else + + #define AP_SIPHASH_DSIZE 8 +@@ -165,21 +163,6 @@ static void ap_siphash24_auth(unsigned char out[AP_SIPHASH_DSIZE], + U64TO8_LE(out, h); + } + +-static int ap_crypto_equals(const void *buf1, const void *buf2, +- apr_size_t size) +-{ +- const unsigned char *p1 = buf1; +- const unsigned char *p2 = buf2; +- unsigned char diff = 0; +- apr_size_t i; +- +- for (i = 0; i < size; ++i) { +- diff |= p1[i] ^ p2[i]; +- } +- +- return 1 & ((diff - 1) >> 8); +-} +- + #endif + + static void compute_auth(const void *src, apr_size_t len, +@@ -404,7 +387,7 @@ static apr_status_t decrypt_string(request_rec * r, const apr_crypto_t *f, + * the MAC and comparing it (timing safe) with the one in the payload. + */ + compute_auth(slider, len, passphrase, passlen, auth); +- if (!ap_crypto_equals(auth, decoded, AP_SIPHASH_DSIZE)) { ++ if (!ap_memeq_timingsafe(auth, decoded, AP_SIPHASH_DSIZE)) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, res, r, APLOGNO(10006) + "auth does not match, skipping"); + continue; +diff --git a/server/util.c b/server/util.c +index 11d0e40..0cdf543 100644 +--- a/server/util.c ++++ b/server/util.c +@@ -3777,3 +3777,141 @@ AP_DECLARE(const char *)ap_dir_fnmatch(ap_dir_match_t *w, const char *path, + + return NULL; + } ++ ++ ++#if APR_VERSION_AT_LEAST(1,8,0) ++AP_DECLARE(int) ap_memeq_timingsafe(const void *buf1, const void *buf2, ++ apr_size_t n) ++{ ++ return apr_memeq_timingsafe(buf1, buf2, n); ++} ++ ++AP_DECLARE(int) ap_streq_timingsafe(const char *sec1, const char *str2) ++{ ++ return apr_streq_timingsafe(sec1, str2); ++} ++ ++AP_DECLARE(int) ap_strneq_timingsafe(const char *sec1, const char *str2, ++ apr_size_t n) ++{ ++ return apr_strneq_timingsafe(sec1, str2, n); ++} ++ ++#else /* !APR_VERSION_AT_LEAST(1,8,0) */ ++ ++/* A volatile variable which is always zero but allows to block the compiler ++ * from optimizing or eliding code using it. Volatile forces the compiler to ++ * emit a memory load for which no value can be assumed, so for instance an ++ * add/sub/xor/or with "optblocker" is a noop that will hide the result to ++ * the optimizer. ++ */ ++static volatile const apr_uint32_t optblocker; ++ ++/* Return whether x is not zero, with no branching controlled by x. ++ * ++ * Taken from the cryptoint library (public domain) by D. J. Bernstein, ++ * which provides timing attacks safe integer operations/primitives. ++ * Code: ++ * https://lib.mceliece.org/libmceliece-20250507/cryptoint/crypto_uint32.h ++ * Paper: ++ * https://cr.yp.to/papers/cryptoint-20250424.pdf ++ */ ++#if __has_attribute(always_inline) ++__attribute__((always_inline)) ++#endif ++static APR_INLINE int test_nonzero_timingsafe(apr_uint32_t x) ++{ ++ x |= -x; /* sets the most significant bit unless x == 0 */ ++ ++ /* shift bit 31 (MSB) to bit 0 */ ++ x >>= 32-6; /* keep 6 bits */ ++ x += optblocker; /* lose the optimizer */ ++ x >>= 5; /* keep the (original) MSB only */ ++ ++ /* x is now 0 or 1 */ ++ return x & INT_MAX; ++} ++ ++AP_DECLARE(int) ap_memeq_timingsafe(const void *buf1, const void *buf2, ++ apr_size_t n) ++{ ++ apr_uint32_t diff = 0; ++ volatile apr_size_t count = n; /* prevent loop unrolling */ ++ apr_size_t i = 0; ++ ++ for (; i < count; ++i) { ++ const unsigned char c1 = ((volatile const unsigned char *)buf1)[i]; ++ const unsigned char c2 = ((volatile const unsigned char *)buf2)[i]; ++ ++ diff |= c1 ^ c2; /* sets diff to non-zero whenever c1 != c2 */ ++ } ++ ++ /* (diff == 0) <=> (diff != 0) ^ 1 */ ++ return test_nonzero_timingsafe(diff) ^ 1; ++} ++ ++AP_DECLARE(int) ap_streq_timingsafe(const char *sec1, const char *str2) ++{ ++ apr_uint32_t diff = 0; ++ apr_size_t i1 = 0, i2 = 0; ++ ++ for (;; ++i2) { ++ const unsigned char c1 = ((volatile const unsigned char *)sec1)[i1]; ++ const unsigned char c2 = ((volatile const unsigned char *)str2)[i2]; ++ ++ diff |= c1 ^ c2; /* sets diff to non-zero whenever c1 != c2 */ ++ ++ /* Not a shortest/longest match because an attacker would usually know ++ * one of the strings and could then determine the length of the other. ++ * So assume only sec1 and its length are secret and stop the loop at ++ * the end of str2. If sec1 is shorter than str2 the loop will continue ++ * by comparing the rest of str2 with the trailing NUL byte of sec1. ++ * In any case since the diff above is computed up to and including a ++ * NUL byte, only the same content and length will raise match. ++ */ ++ if (!c2) { ++ break; ++ } ++ ++ /* Don't go above sec1's NUL byte */ ++ i1 += test_nonzero_timingsafe(c1); ++ } ++ ++ /* (diff == 0) <=> (diff != 0) ^ 1 */ ++ return test_nonzero_timingsafe(diff) ^ 1; ++} ++ ++AP_DECLARE(int) ap_strneq_timingsafe(const char *sec1, const char *str2, ++ apr_size_t n) ++{ ++ apr_uint32_t diff = 0; ++ volatile apr_size_t count = n; /* prevent loop unrolling */ ++ apr_size_t i1 = 0, i2 = 0; ++ ++ for (; i2 < count; ++i2) { ++ const unsigned char c1 = ((volatile const unsigned char *)sec1)[i1]; ++ const unsigned char c2 = ((volatile const unsigned char *)str2)[i2]; ++ ++ diff |= c1 ^ c2; /* sets diff to non-zero whenever c1 != c2 */ ++ ++ /* Not a shortest/longest match because an attacker would usually know ++ * one of the strings and could then determine the length of the other. ++ * So assume only sec1 and its length are secret and stop the loop at ++ * the end of str2. If sec1 is shorter than str2 the loop will continue ++ * by comparing the rest of str2 with the trailing NUL byte of sec1. ++ * In any case since the diff above is computed up to and including a ++ * NUL byte, only the same content and length will raise match. ++ */ ++ if (!c2) { ++ break; ++ } ++ ++ /* Don't go above sec1's NUL byte */ ++ i1 += test_nonzero_timingsafe(c1); ++ } ++ ++ /* (diff == 0) <=> (diff != 0) ^ 1 */ ++ return test_nonzero_timingsafe(diff) ^ 1; ++} ++ ++#endif /* !APR_VERSION_AT_LEAST(1,8,0) */ diff --git a/httpd-2.4.63-CVE-2026-34355.patch b/httpd-2.4.63-CVE-2026-34355.patch new file mode 100644 index 0000000..8b816f8 --- /dev/null +++ b/httpd-2.4.63-CVE-2026-34355.patch @@ -0,0 +1,529 @@ +diff --git a/modules/filters/mod_proxy_html.c b/modules/filters/mod_proxy_html.c +index 4205a61..27614fe 100644 +--- a/modules/filters/mod_proxy_html.c ++++ b/modules/filters/mod_proxy_html.c +@@ -63,6 +63,7 @@ + #include "mod_xml2enc.h" + #include "http_request.h" + #include "ap_expr.h" ++#include "util_varbuf.h" + + /* globals set once at startup */ + static ap_rxplus_t *old_expr; +@@ -122,9 +123,7 @@ typedef struct { + proxy_html_conf *cfg; + htmlParserCtxtPtr parser; + apr_bucket_brigade *bb; +- char *buf; +- size_t offset; +- size_t avail; ++ struct ap_varbuf vb; + const char *encoding; + urlmap *map; + char rbuf[4]; +@@ -132,6 +131,7 @@ typedef struct { + apr_size_t rmin; + } saxctxt; + ++#define DEFAULT_BUFSZ (8192) + + #define NORM_LC 0x1 + #define NORM_MSSLASH 0x2 +@@ -155,17 +155,17 @@ static const char *const xhtml_etag = " />"; + static const char *const DEFAULT_DOCTYPE = ""; + #define DEFAULT_ETAG html_etag + +-static void normalise(unsigned int flags, char *str) ++static void normalise(unsigned int flags, struct ap_varbuf *vb) + { +- char *p; ++ apr_size_t n; + if (flags & NORM_LC) +- for (p = str; *p; ++p) +- if (isupper(*p)) +- *p = tolower(*p); ++ for (n = 0; n < vb->strlen; ++n) ++ vb->buf[n] = apr_tolower(vb->buf[n]); + + if (flags & NORM_MSSLASH) +- for (p = ap_strchr(str, '\\'); p; p = ap_strchr(p+1, '\\')) +- *p = '/'; ++ for (n = 0; n < vb->strlen; ++n) ++ if (vb->buf[n] == '\\') ++ vb->buf[n] = '/'; + + } + #define consume_buffer(ctx,inbuf,bytes,flag) \ +@@ -194,120 +194,97 @@ static void pcharacters(void *ctxt, const xmlChar *uchars, int length) + FLUSH; + } + +-static void preserve(saxctxt *ctx, const size_t len) +-{ +- char *newbuf; +- if (len <= (ctx->avail - ctx->offset)) +- return; +- else while (len > (ctx->avail - ctx->offset)) +- ctx->avail += ctx->cfg->bufsz; +- +- newbuf = realloc(ctx->buf, ctx->avail); +- if (newbuf != ctx->buf) { +- if (ctx->buf) +- apr_pool_cleanup_kill(ctx->f->r->pool, ctx->buf, +- (int(*)(void*))free); +- apr_pool_cleanup_register(ctx->f->r->pool, newbuf, +- (int(*)(void*))free, apr_pool_cleanup_null); +- ctx->buf = newbuf; +- } +-} +- +-static void pappend(saxctxt *ctx, const char *buf, const size_t len) +-{ +- preserve(ctx, len); +- memcpy(ctx->buf+ctx->offset, buf, len); +- ctx->offset += len; +-} +- + static void dump_content(saxctxt *ctx) + { + urlmap *m; + char *found; + size_t s_from, s_to; + size_t match; +- char c = 0; + int nmatch; + ap_regmatch_t pmatch[10]; +- char *subs; + size_t len, offs; + urlmap *themap = ctx->map; ++ struct ap_varbuf temp; + #ifndef GO_FASTER + int verbose = APLOGrtrace1(ctx->f->r); + #endif + +- pappend(ctx, &c, 1); /* append null byte */ +- /* parse the text for URLs */ ++ /* Ensure buffer is null-terminated and strlen is set */ ++ if (ctx->vb.strlen == AP_VARBUF_UNKNOWN) { ++ ctx->vb.strlen = strlen(ctx->vb.buf); ++ } ++ ++ ap_varbuf_init(ctx->f->r->pool, &temp, 0); ++ ++ /* parse the text for URLs */ + for (m = themap; m; m = m->next) { + if (!(m->flags & M_CDATA)) + continue; + if (m->flags & M_REGEX) { ++ temp.strlen = 0; + nmatch = 10; + offs = 0; +- while (!ap_regexec(m->from.r, ctx->buf+offs, nmatch, pmatch, 0)) { ++ while (!ap_regexec(m->from.r, ctx->vb.buf+offs, nmatch, pmatch, 0)) { + match = pmatch[0].rm_so; + s_from = pmatch[0].rm_eo - match; +- subs = ap_pregsub(ctx->f->r->pool, m->to, ctx->buf+offs, +- nmatch, pmatch); +- s_to = strlen(subs); +- len = strlen(ctx->buf); +- offs += match; + VERBOSEB( + const char *f = apr_pstrndup(ctx->f->r->pool, +- ctx->buf + offs, s_from); ++ ctx->vb.buf + offs + match, s_from); + ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, ctx->f->r, +- "C/RX: match at %s, substituting %s", f, subs); ++ "C/RX: match at %s, substituting with pattern %s", ++ f, m->to); + ) +- if (s_to > s_from) { +- preserve(ctx, s_to - s_from); +- memmove(ctx->buf+offs+s_to, ctx->buf+offs+s_from, +- len + 1 - s_from - offs); +- memcpy(ctx->buf+offs, subs, s_to); +- } +- else { +- memcpy(ctx->buf + offs, subs, s_to); +- memmove(ctx->buf+offs+s_to, ctx->buf+offs+s_from, +- len + 1 - s_from - offs); +- } +- offs += s_to; ++ /* Copy text before the match */ ++ ap_varbuf_strmemcat(&temp, ctx->vb.buf+offs, match); ++ /* Append the substitution */ ++ ap_varbuf_regsub(&temp, m->to, ctx->vb.buf+offs, nmatch, pmatch, 0); ++ /* Continue past the match */ ++ offs += pmatch[0].rm_eo; + } ++ /* Copy any remaining text */ ++ ap_varbuf_strcat(&temp, ctx->vb.buf+offs); ++ /* Replace the original buffer with the result */ ++ ctx->vb.strlen = 0; ++ ap_varbuf_strmemcat(&ctx->vb, temp.buf, temp.strlen); + } + else { + s_from = strlen(m->from.c); + s_to = strlen(m->to); +- for (found = strstr(ctx->buf, m->from.c); found; +- found = strstr(ctx->buf+match+s_to, m->from.c)) { +- match = found - ctx->buf; ++ for (found = strstr(ctx->vb.buf, m->from.c); found; ++ found = strstr(ctx->vb.buf+match+s_to, m->from.c)) { ++ match = found - ctx->vb.buf; + if ((m->flags & M_ATSTART) && (match != 0)) + break; +- len = strlen(ctx->buf); ++ len = ctx->vb.strlen; + if ((m->flags & M_ATEND) && (match < (len - s_from))) + continue; + VERBOSE(ap_log_rerror(APLOG_MARK, APLOG_TRACE3, 0, ctx->f->r, + "C: matched %s, substituting %s", + m->from.c, m->to)); + if (s_to > s_from) { +- preserve(ctx, s_to - s_from); +- memmove(ctx->buf+match+s_to, ctx->buf+match+s_from, +- len + 1 - s_from - match); +- memcpy(ctx->buf+match, m->to, s_to); ++ ap_varbuf_grow(&ctx->vb, len + s_to - s_from); ++ memmove(ctx->vb.buf+match+s_to, ctx->vb.buf+match+s_from, ++ len + 1 - s_from - match); ++ memcpy(ctx->vb.buf+match, m->to, s_to); + } + else { +- memcpy(ctx->buf+match, m->to, s_to); +- memmove(ctx->buf+match+s_to, ctx->buf+match+s_from, ++ memcpy(ctx->vb.buf+match, m->to, s_to); ++ memmove(ctx->vb.buf+match+s_to, ctx->vb.buf+match+s_from, + len + 1 - s_from - match); + } ++ ctx->vb.strlen = len - s_from + s_to; + } + } + } +- AP_fwrite(ctx, ctx->buf, strlen(ctx->buf), 1); ++ AP_fwrite(ctx, ctx->vb.buf, ctx->vb.strlen, 1); ++ ap_varbuf_free(&temp); + } + static void pcdata(void *ctxt, const xmlChar *uchars, int length) + { + const char *chars = (const char*) uchars; + saxctxt *ctx = (saxctxt*) ctxt; + if (ctx->cfg->extfix) { +- pappend(ctx, chars, length); ++ ap_varbuf_strmemcat(&ctx->vb, chars, length); + } + else { + /* not sure if this should force-flush +@@ -324,9 +301,9 @@ static void pcomment(void *ctxt, const xmlChar *uchars) + return; + + if (ctx->cfg->extfix) { +- pappend(ctx, "", 3); ++ ap_varbuf_strcat(&ctx->vb, ""); + } + else { + ap_fputs(ctx->f->next, ctx->bb, "