fix: support encoded control zones safely (full percent-decode for zone/group params)#343
fix: support encoded control zones safely (full percent-decode for zone/group params)#343yaoge123 wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves /status/control query parsing by introducing a general percent-decoder for group/zone parameters and removing the prior hard-coded %XX replacement logic, enabling control operations on zones containing a wider range of encoded characters.
Changes:
- Added
ngx_http_vhost_traffic_status_url_decode()helper to percent-decode%XXsequences in-place. - Updated control handler to decode
groupandzonebefore comparisons/lookup and removed the ad-hoc%2A/%3A/%40handling. - Extended Test::Nginx coverage for spaces, punctuation, UTF-8,
+vs%2B, and malformed encodings.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/ngx_http_vhost_traffic_status_string.c |
Adds ngx_http_vhost_traffic_status_url_decode() implementation. |
src/ngx_http_vhost_traffic_status_string.h |
Exposes the new decode helper declaration. |
src/ngx_http_vhost_traffic_status_display.c |
Decodes group/zone before evaluating control command behavior. |
t/008.control_status_zone.t |
Adds coverage for literal + and %2B decoding in status command. |
t/011.control_reset_zone.t |
Adds coverage for literal + and %2B decoding in reset command. |
t/014.control_delete_zone.t |
Adds broader coverage for percent-decoding in delete command (space, punctuation, UTF-8, malformed sequences). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "display_handler_control::copy_str() failed"); | ||
| } | ||
|
|
||
| rc = ngx_http_vhost_traffic_status_url_decode(&decoded_group); | ||
| if (rc != NGX_OK) { | ||
| ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, | ||
| "display_handler_control::url_decode() failed"); |
There was a problem hiding this comment.
After ngx_http_vhost_traffic_status_copy_str() failure, the code logs but continues and immediately calls url_decode() / reads decoded_group.*. If copy_str() returns NGX_ERROR (e.g., OOM), decoded_group.data may be NULL/uninitialized, leading to a crash or undefined behavior. Consider returning NGX_HTTP_INTERNAL_SERVER_ERROR (or at least setting control->command to NONE and skipping further processing) when copy_str() fails, and only calling url_decode() when rc == NGX_OK.
| "display_handler_control::copy_str() failed"); | |
| } | |
| rc = ngx_http_vhost_traffic_status_url_decode(&decoded_group); | |
| if (rc != NGX_OK) { | |
| ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, | |
| "display_handler_control::url_decode() failed"); | |
| "display_handler_control::copy_str() failed"); | |
| return NGX_HTTP_INTERNAL_SERVER_ERROR; | |
| } | |
| rc = ngx_http_vhost_traffic_status_url_decode(&decoded_group); | |
| if (rc != NGX_OK) { | |
| ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, | |
| "display_handler_control::url_decode() failed"); | |
| return NGX_HTTP_INTERNAL_SERVER_ERROR; |
| rc = ngx_http_vhost_traffic_status_copy_str(r->pool, control->zone, &arg_zone); | ||
| if (rc != NGX_OK) { | ||
| ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, | ||
| "display_handler_control::copy_str() failed"); | ||
| } | ||
|
|
||
| ngx_str_set(&encoded_ch, "%2A"); | ||
|
|
||
| rc = ngx_http_vhost_traffic_status_replace_strc(control->zone, &encoded_ch, '*'); | ||
| rc = ngx_http_vhost_traffic_status_url_decode(control->zone); | ||
| if (rc != NGX_OK) { | ||
| ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, | ||
| "display_handler_control::replace_strc() failed"); | ||
| } | ||
|
|
||
| ngx_str_set(&encoded_ch, "%2a"); | ||
|
|
||
| rc = ngx_http_vhost_traffic_status_replace_strc(control->zone, &encoded_ch, '*'); | ||
| if (rc != NGX_OK) { | ||
| ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, | ||
| "display_handler_control::replace_strc() failed"); | ||
| } | ||
|
|
||
| ngx_str_set(&encoded_ch, "%3A"); | ||
|
|
||
| rc = ngx_http_vhost_traffic_status_replace_strc(control->zone, &encoded_ch, ':'); | ||
| if (rc != NGX_OK) { | ||
| ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, | ||
| "display_handler_control::replace_strc() failed"); | ||
| } | ||
|
|
||
| ngx_str_set(&encoded_ch, "%3a"); | ||
|
|
||
| rc = ngx_http_vhost_traffic_status_replace_strc(control->zone, &encoded_ch, ':'); | ||
| if (rc != NGX_OK) { | ||
| ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, | ||
| "display_handler_control::replace_strc() failed"); | ||
| } | ||
|
|
||
| ngx_str_set(&encoded_ch, "%40"); | ||
|
|
||
| rc = ngx_http_vhost_traffic_status_replace_strc(control->zone, &encoded_ch, '@'); | ||
| if (rc != NGX_OK) { | ||
| ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, | ||
| "display_handler_control::replace_strc() failed"); | ||
| "display_handler_control::url_decode() failed"); | ||
| } |
There was a problem hiding this comment.
Same pattern as group: if ngx_http_vhost_traffic_status_copy_str() fails for zone, the function logs but still calls url_decode() and subsequent mutation helpers on control->zone. If copy_str() failed, control->zone->data can be NULL, which can crash. It’s safer to abort the request (500) or skip control processing when copy_str() fails.
| ngx_int_t | ||
| ngx_http_vhost_traffic_status_url_decode(ngx_str_t *buf) | ||
| { | ||
| size_t len; | ||
| u_char *p, *dst; | ||
|
|
||
| p = dst = buf->data; | ||
| len = buf->len; | ||
|
|
||
| while (len > 0) { | ||
| if (*p == '%' && len >= 3 && isxdigit(*(p + 1)) && isxdigit(*(p + 2))) { | ||
| ngx_int_t n; | ||
|
|
||
| n = ngx_hextoi(p + 1, 2); | ||
| if (n != NGX_ERROR) { | ||
| *dst++ = (u_char) n; | ||
| p += 3; | ||
| len -= 3; | ||
| continue; | ||
| } | ||
| } | ||
|
|
||
| *dst++ = *p++; | ||
| len--; | ||
| } | ||
|
|
||
| buf->len = dst - buf->data; | ||
| *(buf->data + buf->len) = '\0'; | ||
|
|
||
| return NGX_OK; |
There was a problem hiding this comment.
ngx_http_vhost_traffic_status_url_decode() always writes a terminating '\0' at buf->data + buf->len without verifying there is space / that the input is already NUL-terminated. Other in-place helpers here (e.g., replace_strc) explicitly check for a trailing NUL before using it. To avoid potential out-of-bounds writes when callers pass an ngx_str_t that isn’t allocated with an extra byte, add a guard (e.g., buf->data != NULL and *(buf->data + original_len) == '\0') and return NGX_ERROR if the precondition isn’t met.
Problem
The
/status/controlAPI parsesgroupandzonequery parameters using hard-coded, character-by-character%XXreplacements. Only three sequences are handled:%2A→*,%3A/%3a→:, and%40→@. As a result:%20), backtick (%60), pipe (%7C),+(encoded as%2B), and any multi-byte UTF-8 sequence all fail to decode — the encoded form never matches a stored zone name.groupparameter duplicates every branch: one for the literal string and one for its%XXform, making the code fragile and hard to extend.Fix
Add
ngx_http_vhost_traffic_status_url_decode()instring.c/string.h— a standard percent-decode routine that:%XXsequences (case-insensitive hex digits).+as a literal+(not a space — this is a path/query parameter, not form data).%sequences (e.g.%2G) untouched.In
display.c, bothgroupandzoneare decoded with this helper before comparison/lookup. All ad-hocreplace_strc()calls and the duplicated%XXbranches are removed.Changes
src/ngx_http_vhost_traffic_status_string.c— addngx_http_vhost_traffic_status_url_decode()src/ngx_http_vhost_traffic_status_string.h— add declarationsrc/ngx_http_vhost_traffic_status_display.c— decodegroupandzonebefore use; remove all hard-coded%XXreplacementsTests
New test cases added to
t/008,t/011, andt/014:::and@in zone (existing tests 6-8, now actually exercising the full decode path)zone=storage%3A%3Alocalhost%40vol0:in upstream@alone zonegroup=upstream%40alone&zone=127.0.0.1%3A1981+stays as literal+zone=storage::localhost@test+value%2Bdecodes to literal+zone=storage::localhost@test%2Bvalue%20zone=storage::localhost@test%20value%60zone=storage::localhost@test%60value%7Czone=storage::localhost@test%7Cvaluezone=storage::localhost@%E5%95%86%E6%A0%87%2Gis left untouchedzone=storage::localhost@test%2Gvalue