mirror of
https://github.com/nginx/nginx.git
synced 2025-06-07 17:52:38 +08:00
HTTP: Strip leading and trailing whitespace from field values
Per RFC9110, HTTP field values never contain leading or trailing whitespace. Strip all such whitespace from HTTP and HTTP field values. The HTTP/1.x parser already stripped spaces but didn't strip tabs, so change the parser to strip tabs as well. In HTTP/2+, the stripping is done during validation. This requires modifying the value. There are three ways to modify the value: 1. Modify the data in-place with memmove(). 2. Move the data pointer to point to after the leading whitespace. 3. Allocate a new buffer and replace the data pointer. Both HPACK and QPACK decompression make a copy of the data, but some code might assume that the data pointer of a field value can safely be passed to ngx_pfree(). Therefore, the first option is chosen. Existing code ensures that header values are NUL-terminated, so the stripping code NUL-pads header values to ensure that the stripped strings have at least as many terminating NUL bytes as they did before being stripped. The stripping code has been tested in a standalone program to make sure that it works correctly, and it correctly strips leading and trailing whitespace from a variety of strings. This code has also been tested with real HTTP/3 requests from Cloudflare's h3i tool. Fixes: #187 Fixes: #598
This commit is contained in:
parent
519407f7e6
commit
210bf0aa29
@ -182,7 +182,11 @@ ngx_int_t ngx_http_huff_decode(u_char *state, u_char *src, size_t len,
|
||||
u_char **dst, ngx_uint_t last, ngx_log_t *log);
|
||||
size_t ngx_http_huff_encode(u_char *src, size_t len, u_char *dst,
|
||||
ngx_uint_t lower);
|
||||
ngx_int_t ngx_http_v23_validate_header(ngx_http_request_t *r,
|
||||
/*
|
||||
* Check if a header name and/or value is valid. If the value is valid,
|
||||
* strip leading and trailing space from it.
|
||||
*/
|
||||
ngx_int_t ngx_http_v23_fixup_header(ngx_http_request_t *r,
|
||||
ngx_str_t *name, ngx_str_t *value);
|
||||
#endif
|
||||
|
||||
|
@ -10,6 +10,11 @@
|
||||
#include <ngx_http.h>
|
||||
|
||||
|
||||
#if (NGX_HTTP_V2 || NGX_HTTP_V3)
|
||||
static inline ngx_int_t ngx_isspace(u_char ch);
|
||||
#endif
|
||||
|
||||
|
||||
static uint32_t usual[] = {
|
||||
0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */
|
||||
|
||||
@ -972,6 +977,7 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b,
|
||||
case sw_space_before_value:
|
||||
switch (ch) {
|
||||
case ' ':
|
||||
case '\t':
|
||||
break;
|
||||
case CR:
|
||||
r->header_start = p;
|
||||
@ -996,6 +1002,7 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b,
|
||||
case sw_value:
|
||||
switch (ch) {
|
||||
case ' ':
|
||||
case '\t':
|
||||
r->header_end = p;
|
||||
state = sw_space_after_value;
|
||||
break;
|
||||
@ -1016,6 +1023,7 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b,
|
||||
case sw_space_after_value:
|
||||
switch (ch) {
|
||||
case ' ':
|
||||
case '\t':
|
||||
break;
|
||||
case CR:
|
||||
state = sw_almost_done;
|
||||
@ -1091,11 +1099,21 @@ header_done:
|
||||
|
||||
|
||||
#if (NGX_HTTP_V2 || NGX_HTTP_V3)
|
||||
|
||||
|
||||
static inline ngx_int_t
|
||||
ngx_isspace(u_char ch)
|
||||
{
|
||||
return ch == ' ' || ch == '\t';
|
||||
}
|
||||
|
||||
|
||||
ngx_int_t
|
||||
ngx_http_v23_validate_header(ngx_http_request_t *r, ngx_str_t *name,
|
||||
ngx_http_v23_fixup_header(ngx_http_request_t *r, ngx_str_t *name,
|
||||
ngx_str_t *value)
|
||||
{
|
||||
u_char ch;
|
||||
ngx_str_t tmp;
|
||||
ngx_uint_t i;
|
||||
ngx_http_core_srv_conf_t *cscf;
|
||||
|
||||
@ -1134,6 +1152,11 @@ ngx_http_v23_validate_header(ngx_http_request_t *r, ngx_str_t *name,
|
||||
r->invalid_header = 1;
|
||||
}
|
||||
|
||||
/* Keep subsequent code from having to special-case empty strings. */
|
||||
if (value->len == 0) {
|
||||
return NGX_OK;
|
||||
}
|
||||
|
||||
for (i = 0; i != value->len; i++) {
|
||||
ch = value->data[i];
|
||||
|
||||
@ -1147,6 +1170,52 @@ ngx_http_v23_validate_header(ngx_http_request_t *r, ngx_str_t *name,
|
||||
}
|
||||
}
|
||||
|
||||
tmp = *value;
|
||||
|
||||
if (!ngx_isspace(tmp.data[0])
|
||||
&& !ngx_isspace(tmp.data[tmp.len - 1])) {
|
||||
/* Fast path: nothing to strip. */
|
||||
return NGX_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* Strip trailing whitespace. Do this first so that
|
||||
* if the string is all whitespace, tmp.data is not a
|
||||
* past-the-end pointer, which cannot be safely passed
|
||||
* to memmove(). After the loop, the string is either
|
||||
* empty or ends with a non-whitespace character.
|
||||
*/
|
||||
while (tmp.len && ngx_isspace(tmp.data[tmp.len - 1])) {
|
||||
tmp.len--;
|
||||
}
|
||||
|
||||
/* Strip leading whitespace */
|
||||
if (tmp.len && ngx_isspace(tmp.data[0])) {
|
||||
/*
|
||||
* Last loop guaranteed that 'tmp' does not end with whitespace, and
|
||||
* this check guarantees it is not empty and starts with whitespace.
|
||||
* Therefore, 'tmp' must end with a non-whitespace character, and must
|
||||
* be of length at least 2. This means that it is safe to keep going
|
||||
* until a non-whitespace character is found.
|
||||
*/
|
||||
do {
|
||||
tmp.len--;
|
||||
tmp.data++;
|
||||
} while (ngx_isspace(tmp.data[0]));
|
||||
|
||||
/* Move remaining string to start of buffer. */
|
||||
memmove(value->data, tmp.data, tmp.len);
|
||||
}
|
||||
|
||||
/*
|
||||
* NUL-pad the data, so that if it was NUL-terminated before, it stil is.
|
||||
* At least one byte will have been stripped, so value->data + tmp.len
|
||||
* is not a past-the-end pointer.
|
||||
*/
|
||||
memset(value->data + tmp.len, '\0', value->len - tmp.len);
|
||||
|
||||
/* Fix up length and return. */
|
||||
value->len = tmp.len;
|
||||
return NGX_OK;
|
||||
}
|
||||
#endif
|
||||
|
@ -1772,8 +1772,7 @@ ngx_http_v2_state_process_header(ngx_http_v2_connection_t *h2c, u_char *pos,
|
||||
fc = r->connection;
|
||||
|
||||
/* TODO Optimization: validate headers while parsing. */
|
||||
if (ngx_http_v23_validate_header(r, &header->name, &header->value)
|
||||
!= NGX_OK) {
|
||||
if (ngx_http_v23_fixup_header(r, &header->name, &header->value) != NGX_OK) {
|
||||
ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
|
||||
goto error;
|
||||
}
|
||||
|
@ -630,7 +630,7 @@ ngx_http_v3_process_header(ngx_http_request_t *r, ngx_str_t *name,
|
||||
|
||||
r->v3_parse->header_limit -= len;
|
||||
|
||||
if (ngx_http_v23_validate_header(r, name, value) != NGX_OK) {
|
||||
if (ngx_http_v23_fixup_header(r, name, value) != NGX_OK) {
|
||||
ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
|
||||
return NGX_ERROR;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user