[PATCH 2 of 3] Script: buffer overrun protection

Maxim Dounin mdounin at mdounin.ru
Mon Jun 8 17:37:22 UTC 2026


# HG changeset patch
# User Maxim Dounin <mdounin at mdounin.ru>
# Date 1780936496 -10800
#      Mon Jun 08 19:34:56 2026 +0300
# Node ID 1794ecca0620d0e54f5649c3405ea010e8abce2e
# Parent  ee56583e9b7e2d80bb5d6efc18147cdb13295b43
Script: buffer overrun protection.

With this change, all script copy operations now check if there is
enough room in the buffer.  To do so, the script engine now provides the
e->end pointer, which specifies expected buffer end, and each copy
operation is checked against it with the ngx_http_script_check_length()
function.

The e->end pointer is optional and only checked when set, thus
introducing no incompatible API changes.  All standard functions were
updated to use it, notably ngx_http_complex_value(), ngx_http_script_run(),
ngx_http_script_regex_start_code(), ngx_http_script_complex_value_code().
Direct script evaluation in the proxy, fastcgi, scgi, uwsgi, grpc proxy,
index, and try_files modules will be updated by a separate patch.

In particular, this catches issues as observed when evaluating variables
with side effects, such as in the following configuration:

    map $uri $map {
        ~(?<capture>.*) $capture;
    }

    set $capture "";
    set $temp "$capture $map";

As well as when evaluating non-cacheable variables, where length of a
variable might change between length and copy codes, such as in the
following configuration:

    map prefix:$capture $map_volatile {
        volatile;
        ~(?<capture>.*) $capture;
    }

    set $capture "";
    set $temp "$map_volatile";

Similar changes were made in the stream module.

diff --git a/src/http/ngx_http_script.c b/src/http/ngx_http_script.c
--- a/src/http/ngx_http_script.c
+++ b/src/http/ngx_http_script.c
@@ -91,12 +91,17 @@ ngx_http_complex_value(ngx_http_request_
     e.ip = val->values;
     e.pos = value->data;
     e.buf = *value;
+    e.end = value->data + len;
 
     while (*(uintptr_t *) e.ip) {
         code = *(ngx_http_script_code_pt *) e.ip;
         code((ngx_http_script_engine_t *) &e);
     }
 
+    if (e.status) {
+        return NGX_ERROR;
+    }
+
     *value = e.buf;
 
     return NGX_OK;
@@ -641,12 +646,17 @@ ngx_http_script_run(ngx_http_request_t *
 
     e.ip = code_values;
     e.pos = value->data;
+    e.end = value->data + len;
 
     while (*(uintptr_t *) e.ip) {
         code = *(ngx_http_script_code_pt *) e.ip;
         code((ngx_http_script_engine_t *) &e);
     }
 
+    if (e.status) {
+        return NULL;
+    }
+
     return e.pos;
 }
 
@@ -794,6 +804,25 @@ ngx_http_script_add_code(ngx_array_t *co
 }
 
 
+ngx_int_t
+ngx_http_script_check_length(ngx_http_script_engine_t *e, size_t len)
+{
+    if (e->end == NULL) {
+        return NGX_OK;
+    }
+
+    if (e->end - e->pos < (ssize_t) len) {
+        ngx_log_error(NGX_LOG_ALERT, e->request->connection->log, 0,
+                      "no buffer space in script copy");
+        e->ip = ngx_http_script_exit;
+        e->status = NGX_HTTP_INTERNAL_SERVER_ERROR;
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
 static ngx_int_t
 ngx_http_script_add_copy_code(ngx_http_script_compile_t *sc, ngx_str_t *value,
     ngx_uint_t last)
@@ -862,6 +891,11 @@ ngx_http_script_copy_code(ngx_http_scrip
     p = e->pos;
 
     if (!e->skip) {
+
+        if (ngx_http_script_check_length(e, code->len) != NGX_OK) {
+            return;
+        }
+
         e->pos = ngx_copy(p, e->ip + sizeof(ngx_http_script_copy_code_t),
                           code->len);
     }
@@ -965,6 +999,11 @@ ngx_http_script_copy_var_code(ngx_http_s
         }
 
         if (value && !value->not_found) {
+
+            if (ngx_http_script_check_length(e, value->len) != NGX_OK) {
+                return;
+            }
+
             p = e->pos;
             e->pos = ngx_copy(p, value->data, value->len);
 
@@ -1159,6 +1198,7 @@ ngx_http_script_regex_start_code(ngx_htt
     e->quote = code->redirect;
 
     e->pos = e->buf.data;
+    e->end = e->buf.data + e->buf.len;
 
     e->ip += sizeof(ngx_http_script_regex_code_t);
 }
@@ -1196,6 +1236,11 @@ ngx_http_script_regex_end_code(ngx_http_
         e->pos = dst;
 
         if (code->add_args && r->args.len) {
+
+            if (ngx_http_script_check_length(e, r->args.len + 1) != NGX_OK) {
+                return;
+            }
+
             *e->pos++ = (u_char) (code->args ? '&' : '?');
             e->pos = ngx_copy(e->pos, r->args.data, r->args.len);
         }
@@ -1229,6 +1274,11 @@ ngx_http_script_regex_end_code(ngx_http_
         e->buf.len = e->args - e->buf.data;
 
         if (code->add_args && r->args.len) {
+
+            if (ngx_http_script_check_length(e, r->args.len + 1) != NGX_OK) {
+                return;
+            }
+
             *e->pos++ = '&';
             e->pos = ngx_copy(e->pos, r->args.data, r->args.len);
         }
@@ -1350,6 +1400,7 @@ ngx_http_script_copy_capture_code(ngx_ht
     int                                  *cap;
     u_char                               *p, *pos;
     size_t                                len;
+    uintptr_t                             escape;
     ngx_uint_t                            n;
     ngx_http_request_t                   *r;
     ngx_http_script_copy_capture_code_t  *code;
@@ -1373,9 +1424,20 @@ ngx_http_script_copy_capture_code(ngx_ht
         if ((e->is_args || e->quote)
             && (e->request->quoted_uri || e->request->plus_in_uri))
         {
+            escape = 2 * ngx_escape_uri(NULL, p, len, NGX_ESCAPE_ARGS);
+
+            if (ngx_http_script_check_length(e, len + escape) != NGX_OK) {
+                return;
+            }
+
             e->pos = (u_char *) ngx_escape_uri(pos, p, len, NGX_ESCAPE_ARGS);
 
         } else {
+
+            if (ngx_http_script_check_length(e, len) != NGX_OK) {
+                return;
+            }
+
             e->pos = ngx_copy(pos, p, len);
         }
     }
@@ -1743,6 +1805,7 @@ ngx_http_script_complex_value_code(ngx_h
     }
 
     e->pos = e->buf.data;
+    e->end = e->buf.data + len;
 
     e->sp->len = e->buf.len;
     e->sp->data = e->buf.data;
diff --git a/src/http/ngx_http_script.h b/src/http/ngx_http_script.h
--- a/src/http/ngx_http_script.h
+++ b/src/http/ngx_http_script.h
@@ -17,6 +17,7 @@
 typedef struct {
     u_char                     *ip;
     u_char                     *pos;
+    u_char                     *end;
     ngx_http_variable_value_t  *sp;
 
     ngx_str_t                   buf;
@@ -231,6 +232,9 @@ void *ngx_http_script_start_code(ngx_poo
     size_t size);
 void *ngx_http_script_add_code(ngx_array_t *codes, size_t size, void *code);
 
+ngx_int_t ngx_http_script_check_length(ngx_http_script_engine_t *e,
+    size_t len);
+
 size_t ngx_http_script_copy_len_code(ngx_http_script_engine_t *e);
 void ngx_http_script_copy_code(ngx_http_script_engine_t *e);
 size_t ngx_http_script_copy_var_len_code(ngx_http_script_engine_t *e);
diff --git a/src/stream/ngx_stream_script.c b/src/stream/ngx_stream_script.c
--- a/src/stream/ngx_stream_script.c
+++ b/src/stream/ngx_stream_script.c
@@ -90,6 +90,7 @@ ngx_stream_complex_value(ngx_stream_sess
 
     e.ip = val->values;
     e.pos = value->data;
+    e.end = value->data + len;
     e.buf = *value;
 
     while (*(uintptr_t *) e.ip) {
@@ -97,6 +98,10 @@ ngx_stream_complex_value(ngx_stream_sess
         code((ngx_stream_script_engine_t *) &e);
     }
 
+    if (e.status) {
+        return NGX_ERROR;
+    }
+
     *value = e.buf;
 
     return NGX_OK;
@@ -522,12 +527,17 @@ ngx_stream_script_run(ngx_stream_session
 
     e.ip = code_values;
     e.pos = value->data;
+    e.end = value->data + len;
 
     while (*(uintptr_t *) e.ip) {
         code = *(ngx_stream_script_code_pt *) e.ip;
         code((ngx_stream_script_engine_t *) &e);
     }
 
+    if (e.status) {
+        return NULL;
+    }
+
     return e.pos;
 }
 
@@ -662,6 +672,25 @@ ngx_stream_script_add_code(ngx_array_t *
 }
 
 
+ngx_int_t
+ngx_stream_script_check_length(ngx_stream_script_engine_t *e, size_t len)
+{
+    if (e->end == NULL) {
+        return NGX_OK;
+    }
+
+    if (e->end - e->pos < (ssize_t) len) {
+        ngx_log_error(NGX_LOG_ALERT, e->session->connection->log, 0,
+                      "no buffer space in script copy");
+        e->ip = ngx_stream_script_exit;
+        e->status = NGX_STREAM_INTERNAL_SERVER_ERROR;
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
 static ngx_int_t
 ngx_stream_script_add_copy_code(ngx_stream_script_compile_t *sc,
     ngx_str_t *value, ngx_uint_t last)
@@ -731,6 +760,11 @@ ngx_stream_script_copy_code(ngx_stream_s
     p = e->pos;
 
     if (!e->skip) {
+
+        if (ngx_stream_script_check_length(e, code->len) != NGX_OK) {
+            return;
+        }
+
         e->pos = ngx_copy(p, e->ip + sizeof(ngx_stream_script_copy_code_t),
                           code->len);
     }
@@ -835,6 +869,11 @@ ngx_stream_script_copy_var_code(ngx_stre
         }
 
         if (value && !value->not_found) {
+
+            if (ngx_stream_script_check_length(e, value->len) != NGX_OK) {
+                return;
+            }
+
             p = e->pos;
             e->pos = ngx_copy(p, value->data, value->len);
 
@@ -935,6 +974,11 @@ ngx_stream_script_copy_capture_code(ngx_
         cap = s->captures;
         len = cap[n + 1] - cap[n];
         p = s->captures_data + cap[n];
+
+        if (ngx_stream_script_check_length(e, len) != NGX_OK) {
+            return;
+        }
+
         e->pos = ngx_copy(pos, p, len);
     }
 
diff --git a/src/stream/ngx_stream_script.h b/src/stream/ngx_stream_script.h
--- a/src/stream/ngx_stream_script.h
+++ b/src/stream/ngx_stream_script.h
@@ -17,6 +17,7 @@
 typedef struct {
     u_char                       *ip;
     u_char                       *pos;
+    u_char                       *end;
     ngx_stream_variable_value_t  *sp;
 
     ngx_str_t                     buf;
@@ -25,6 +26,7 @@ typedef struct {
     unsigned                      flushed:1;
     unsigned                      skip:1;
 
+    ngx_int_t                     status;
     ngx_stream_session_t         *session;
 } ngx_stream_script_engine_t;
 
@@ -127,6 +129,9 @@ void ngx_stream_script_flush_no_cacheabl
 
 void *ngx_stream_script_add_code(ngx_array_t *codes, size_t size, void *code);
 
+ngx_int_t ngx_stream_script_check_length(ngx_stream_script_engine_t *e,
+    size_t len);
+
 size_t ngx_stream_script_copy_len_code(ngx_stream_script_engine_t *e);
 void ngx_stream_script_copy_code(ngx_stream_script_engine_t *e);
 size_t ngx_stream_script_copy_var_len_code(ngx_stream_script_engine_t *e);



More information about the nginx-devel mailing list