CVE-2020-11724 (Other, published 2020-04-12T21:15Z)
Description: value: An issue was discovered in OpenResty before 1.15.8.4. ngx_http_lua_subrequest.c allows HTTP request smuggling, as demonstrated by the ngx.location.capture API.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: CWE-444 \u2014 Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')
The product acts as an intermediary HTTP agent
         (such as a proxy or firewall) in the data flow between two
         entities such as a client and server, but it does not
         interpret malformed HTTP requests or responses in ways that
         are consistent with how the messages will be processed by
         those entities that are at the ultimate destination.
Repository: https://github.com/openresty/lua-nginx-module
Files changed:
src/ngx_http_lua_subrequest.c
t/020-subrequest.t
Commit message: bugfix: prevented request smuggling in the ngx.location.capture API.

Signed-off-by: Yichun Zhang (agentzh) <yichun@openresty.com>


Vulnerable code:
static ngx_int_t ngx_http_lua_set_content_length_header(ngx_http_request_t *r,
    off_t len);
    ngx_http_request_t *r);
    ngx_int_t                    rc;
        rc = ngx_http_lua_set_content_length_header(sr,
                                                    body->buf
                                                    ? ngx_buf_size(body->buf)
                                                    : 0);

        if (rc != NGX_OK) {
            return NGX_ERROR;
        }

        rc = ngx_http_lua_set_content_length_header(sr, 0);
        if (rc != NGX_OK) {
            return NGX_ERROR;
        }

#if 1
#endif
        if (ngx_http_lua_copy_request_headers(sr, r) != NGX_OK) {
            return NGX_ERROR;
        if (sr->request_body) {
            if (sr->request_body->temp_file) {
                if (ngx_http_lua_copy_in_file_request_body(sr) != NGX_OK) {
                    return NGX_ERROR;
                }
static ngx_int_t
ngx_http_lua_set_content_length_header(ngx_http_request_t *r, off_t len)
{
    ngx_table_elt_t                 *h, *header;
    u_char                          *p;
    ngx_list_part_t                 *part;
    ngx_http_request_t              *pr;
    ngx_uint_t                       i;

    r->headers_in.content_length_n = len;

    if (ngx_list_init(&r->headers_in.headers, r->pool, 20,
                      sizeof(ngx_table_elt_t)) != NGX_OK)
    {
        return NGX_ERROR;
    }

    h = ngx_list_push(&r->headers_in.headers);
    if (h == NULL) {
        return NGX_ERROR;
    }

    h->key = ngx_http_lua_content_length_header_key;
    h->lowcase_key = ngx_pnalloc(r->pool, h->key.len);
    if (h->lowcase_key == NULL) {
        return NGX_ERROR;
    }

    ngx_strlow(h->lowcase_key, h->key.data, h->key.len);

    r->headers_in.content_length = h;

    p = ngx_palloc(r->pool, NGX_OFF_T_LEN);
    if (p == NULL) {
        return NGX_ERROR;
    }

    h->value.data = p;

    h->value.len = ngx_sprintf(h->value.data, "%O", len) - h->value.data;

    h->hash = ngx_http_lua_content_length_hash;

#if 0
    dd("content length hash: %lu == %lu", (unsigned long) h->hash,
       ngx_hash_key_lc((u_char *) "Content-Length",
                       sizeof("Content-Length") - 1));
#endif

    dd("r content length: %.*s",
       (int) r->headers_in.content_length->value.len,
       r->headers_in.content_length->value.data);

    pr = r->parent;

    if (pr == NULL) {
        return NGX_OK;
    }

    /* forward the parent request's all other request headers */

    part = &pr->headers_in.headers.part;
    header = part->elts;

    for (i = 0; /* void */; i++) {

        if (i >= part->nelts) {
            if (part->next == NULL) {
                break;
            }

            part = part->next;
            header = part->elts;
            i = 0;
        }

        if (header[i].key.len == sizeof("Content-Length") - 1
            && ngx_strncasecmp(header[i].key.data, (u_char *) "Content-Length",
                               sizeof("Content-Length") - 1) == 0)
        {
            continue;
        }

        if (ngx_http_lua_set_input_header(r, header[i].key,
                                          header[i].value, 0) == NGX_ERROR)
        {
            return NGX_ERROR;
        }
    }

    return NGX_OK;
}


ngx_http_lua_copy_request_headers(ngx_http_request_t *sr, ngx_http_request_t *r)
    ngx_table_elt_t                 *header;
    dd("before: parent req headers count: %d",
       (int) r->headers_in.headers.part.nelts);
    part = &r->headers_in.headers.part;
        dd("setting request header %.*s: %.*s", (int) header[i].key.len,
       (int) r->headers_in.headers.part.nelts);
=== TEST 8: PUT (nobody, proxy method)
=== TEST 9: PUT (nobody, no proxy method)
=== TEST 10: PUT (nobody, no proxy method)

Fixed code:
    ngx_http_request_t *pr, int pr_not_chunked);
    int                          pr_not_chunked = 0;
        if (!r->headers_in.chunked) {
            pr_not_chunked = 1;
        if (sr->request_body && sr->request_body->temp_file) {
            if (ngx_http_lua_copy_in_file_request_body(sr) != NGX_OK) {
                return NGX_ERROR;
    if (ngx_http_lua_copy_request_headers(sr, r, pr_not_chunked) != NGX_OK) {
        return NGX_ERROR;
    }

ngx_http_lua_copy_request_headers(ngx_http_request_t *sr,
    ngx_http_request_t *pr, int pr_not_chunked)
    ngx_table_elt_t                 *clh, *header;
    u_char                          *p;
    off_t                            len;

    dd("before: parent req headers count: %d",
       (int) pr->headers_in.headers.part.nelts);
    if (sr->request_body && !pr_not_chunked) {

        /* craft our own Content-Length */

        len = sr->request_body->buf ? ngx_buf_size(sr->request_body->buf) : 0;

        clh = ngx_list_push(&sr->headers_in.headers);
        if (clh == NULL) {
            return NGX_ERROR;
        }
        clh->hash = ngx_http_lua_content_length_hash;
        clh->key = ngx_http_lua_content_length_header_key;
        clh->lowcase_key = ngx_pnalloc(sr->pool, clh->key.len);
        if (clh->lowcase_key == NULL) {
            return NGX_ERROR;
        }

        ngx_strlow(clh->lowcase_key, clh->key.data, clh->key.len);

        p = ngx_palloc(sr->pool, NGX_OFF_T_LEN);
        if (p == NULL) {
            return NGX_ERROR;
        }

        clh->value.data = p;
        clh->value.len = ngx_sprintf(clh->value.data, "%O", len)
                         - clh->value.data;

        sr->headers_in.content_length = clh;
        sr->headers_in.content_length_n = len;

        dd("sr crafted content-length: %.*s",
           (int) sr->headers_in.content_length->value.len,
           sr->headers_in.content_length->value.data);
    }

    /* copy the parent request's headers */

    part = &pr->headers_in.headers.part;
        if (!pr_not_chunked && header[i].key.len == sizeof("Content-Length") - 1
            && ngx_strncasecmp(header[i].key.data, (u_char *) "Content-Length",
                               sizeof("Content-Length") - 1) == 0)
        {
            continue;
        }

        dd("sr copied req header %.*s: %.*s", (int) header[i].key.len,
       (int) pr->headers_in.headers.part.nelts);

$ENV{TEST_NGINX_HTML_DIR} ||= html_dir();
=== TEST 8: PUT (with body, proxy method)
=== TEST 9: PUT (with body, no proxy method)
=== TEST 10: PUT (no body, no proxy method)



=== TEST 77: avoid request smuggling 1/4 (default capture + smuggle in header)
--- http_config
    upstream backend {
        server unix:$TEST_NGINX_HTML_DIR/nginx.sock;
        keepalive 32;
    }

    server {
        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;

        location / {
            content_by_lua_block {
                ngx.say("method: ", ngx.var.request_method,
                        ", uri: ", ngx.var.uri,
                        ", X: ", ngx.var.http_x)
            }
        }
    }
--- config
    location /proxy {
        proxy_http_version 1.1;
        proxy_set_header   Connection "";
        proxy_pass         http://backend/foo;
    }

    location /capture {
        server_tokens off;
        more_clear_headers Date;

        content_by_lua_block {
            local res = ngx.location.capture("/proxy")
            ngx.print(res.body)
        }
    }

    location /t {
        content_by_lua_block {
            local req = [[
GET /capture HTTP/1.1
Host: test.com
Content-Length: 37
Transfer-Encoding: chunked

0

GET /capture HTTP/1.1
Host: test.com
X: GET /bar HTTP/1.0

]]

            local sock = ngx.socket.tcp()
            sock:settimeout(1000)

            local ok, err = sock:connect("127.0.0.1", $TEST_NGINX_SERVER_PORT)
            if not ok then
                ngx.say("failed to connect: ", err)
                return
            end

            local bytes, err = sock:send(req)
            if not bytes then
                ngx.say("failed to send req: ", err)
                return
            end

            ngx.say("req bytes: ", bytes)

            local n_resp = 0

            local reader = sock:receiveuntil("\r\n")
            while true do
                local line, err = reader()
                if line then
                    ngx.say(line)
                    if line == "0" then
                        n_resp = n_resp + 1
                    end

                    if n_resp >= 2 then
                        break
                    end

                else
                    ngx.say("err: ", err)
                    break
                end
            end

            sock:close()
        }
    }
--- request
GET /t
--- response_body
req bytes: 146
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: keep-alive

1f
method: GET, uri: /foo, X: nil

0

HTTP/1.1 200 OK
Server: nginx
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: keep-alive

2d
method: GET, uri: /foo, X: GET /bar HTTP/1.0

0
--- no_error_log
[error]



=== TEST 78: avoid request smuggling 2/4 (POST capture + smuggle in body)
--- http_config
    upstream backend {
        server unix:$TEST_NGINX_HTML_DIR/nginx.sock;
        keepalive 32;
    }

    server {
        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;

        location / {
            content_by_lua_block {
                ngx.say("method: ", ngx.var.request_method,
                        ", uri: ", ngx.var.uri)
            }
        }
    }
--- config
    location /proxy {
        proxy_http_version 1.1;
        proxy_set_header   Connection "";
        proxy_pass         http://backend/foo;
    }

    location /capture {
        server_tokens off;
        more_clear_headers Date;

        content_by_lua_block {
            ngx.req.read_body()
            local res = ngx.location.capture("/proxy", { method = ngx.HTTP_POST })
            ngx.print(res.body)
        }
    }

    location /t {
        content_by_lua_block {
            local req = [[
GET /capture HTTP/1.1
Host: test.com
Content-Length: 57
Transfer-Encoding: chunked

0

POST /capture HTTP/1.1
Host: test.com
Content-Length: 60

POST /bar HTTP/1.1
Host: test.com
Content-Length: 5

hello

]]

            local sock = ngx.socket.tcp()
            sock:settimeout(1000)

            local ok, err = sock:connect("127.0.0.1", $TEST_NGINX_SERVER_PORT)
            if not ok then
                ngx.say("failed to connect: ", err)
                return
            end

            local bytes, err = sock:send(req)
            if not bytes then
                ngx.say("failed to send req: ", err)
                return
            end

            ngx.say("req bytes: ", bytes)

            local n_resp = 0

            local reader = sock:receiveuntil("\r\n")
            while true do
                local line, err = reader()
                if line then
                    ngx.say(line)
                    if line == "0" then
                        n_resp = n_resp + 1
                    end

                    if n_resp >= 2 then
                        break
                    end

                else
                    ngx.say("err: ", err)
                    break
                end
            end

            sock:close()
        }
    }
--- request
GET /t
--- response_body
req bytes: 205
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: keep-alive

18
method: POST, uri: /foo

0

HTTP/1.1 200 OK
Server: nginx
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: keep-alive

18
method: POST, uri: /foo

0
--- no_error_log
[error]



=== TEST 79: avoid request smuggling 3/4 (POST capture w/ always_forward_body + smuggle in body)
--- http_config
    upstream backend {
        server unix:$TEST_NGINX_HTML_DIR/nginx.sock;
        keepalive 32;
    }

    server {
        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;

        location / {
            content_by_lua_block {
                ngx.say("method: ", ngx.var.request_method,
                        ", uri: ", ngx.var.uri)
            }
        }
    }
--- config
    location /proxy {
        proxy_http_version 1.1;
        proxy_set_header   Connection "";
        proxy_pass         http://backend/foo;
    }

    location /capture {
        server_tokens off;
        more_clear_headers Date;

        content_by_lua_block {
            ngx.req.read_body()
            local res = ngx.location.capture("/proxy", {
                method = ngx.HTTP_POST,
                always_forward_body = true
            })
            ngx.print(res.body)
        }
    }

    location /t {
        content_by_lua_block {
            local req = [[
GET /capture HTTP/1.1
Host: test.com
Content-Length: 57
Transfer-Encoding: chunked

0

POST /capture HTTP/1.1
Host: test.com
Content-Length: 60

POST /bar HTTP/1.1
Host: test.com
Content-Length: 5

hello

]]

            local sock = ngx.socket.tcp()
            sock:settimeout(1000)

            local ok, err = sock:connect("127.0.0.1", $TEST_NGINX_SERVER_PORT)
            if not ok then
                ngx.say("failed to connect: ", err)
                return
            end

            local bytes, err = sock:send(req)
            if not bytes then
                ngx.say("failed to send req: ", err)
                return
            end

            ngx.say("req bytes: ", bytes)

            local n_resp = 0

            local reader = sock:receiveuntil("\r\n")
            while true do
                local line, err = reader()
                if line then
                    ngx.say(line)
                    if line == "0" then
                        n_resp = n_resp + 1
                    end

                    if n_resp >= 2 then
                        break
                    end

                else
                    ngx.say("err: ", err)
                    break
                end
            end

            sock:close()
        }
    }
--- request
GET /t
--- response_body
req bytes: 205
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: keep-alive

18
method: POST, uri: /foo

0

HTTP/1.1 200 OK
Server: nginx
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: keep-alive

18
method: POST, uri: /foo

0
--- no_error_log
[error]



=== TEST 80: avoid request smuggling 4/4 (POST capture w/ body + smuggle in body)
--- http_config
    upstream backend {
        server unix:$TEST_NGINX_HTML_DIR/nginx.sock;
        keepalive 32;
    }

    server {
        listen unix:$TEST_NGINX_HTML_DIR/nginx.sock;

        location / {
            content_by_lua_block {
                ngx.say("method: ", ngx.var.request_method,
                        ", uri: ", ngx.var.uri)
            }
        }
    }
--- config
    location /proxy {
        proxy_http_version 1.1;
        proxy_set_header   Connection "";
        proxy_pass         http://backend/foo;
    }

    location /capture {
        server_tokens off;
        more_clear_headers Date;

        content_by_lua_block {
            ngx.req.read_body()
            local res = ngx.location.capture("/proxy", {
                method = ngx.HTTP_POST,
                always_forward_body = true,
                body = ngx.req.get_body_data()
            })
            ngx.print(res.body)
        }
    }

    location /t {
        content_by_lua_block {
            local req = [[
GET /capture HTTP/1.1
Host: test.com
Content-Length: 57
Transfer-Encoding: chunked

0

POST /capture HTTP/1.1
Host: test.com
Content-Length: 60

POST /bar HTTP/1.1
Host: test.com
Content-Length: 5

hello

]]

            local sock = ngx.socket.tcp()
            sock:settimeout(1000)

            local ok, err = sock:connect("127.0.0.1", $TEST_NGINX_SERVER_PORT)
            if not ok then
                ngx.say("failed to connect: ", err)
                return
            end

            local bytes, err = sock:send(req)
            if not bytes then
                ngx.say("failed to send req: ", err)
                return
            end

            ngx.say("req bytes: ", bytes)

            local n_resp = 0

            local reader = sock:receiveuntil("\r\n")
            while true do
                local line, err = reader()
                if line then
                    ngx.say(line)
                    if line == "0" then
                        n_resp = n_resp + 1
                    end

                    if n_resp >= 2 then
                        break
                    end

                else
                    ngx.say("err: ", err)
                    break
                end
            end

            sock:close()
        }
    }
--- request
GET /t
--- response_body
req bytes: 205
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: keep-alive

18
method: POST, uri: /foo

0

HTTP/1.1 200 OK
Server: nginx
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: keep-alive

18
method: POST, uri: /foo

0
--- no_error_log
[error]