Skip to content

Capture the request body #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -145,6 +145,32 @@ all empty strings are by default escaped to `NULL` value. This behavior can be
disabled by prefixing `$unescaped` string with `=` sign.


postgres_escape_request_body
-----------------------
* **syntax**: `postgres_request_body_escape on/off`
* **default**: `on`
* **context**: `location`

The flag provides to double each single quote symbol
in the complex SQL statements that contain substitutions from client http request body.
Disabling this option is pretty dangerous.

Please make sure you are NOT using postgres configuration option `standard_conforming_strings: off` (default for PostgreSQL < 9.1).
Using `standard_conforming_strings: off` will make completely worthless the injection protection and allows an attacker to use control characters to change a query string.

To check this option use: (correct setting below)
```
postgres=# SHOW standard_conforming_strings;

Set timeout for receiving result from the database.

standard_conforming_strings
-----------------------------
on
(1 row)
```


postgres_connect_timeout
------------------------
* **syntax**: `postgres_connect_timeout timeout`
@@ -357,6 +383,24 @@ Required modules (other than `ngx_postgres`):

- [ngx_set_misc](http://github.com/agentzh/set-misc-nginx-module).


Sample configuration #7
-----------------------
Use POST data to parameter in SQL query.

location /sms/(?<id>\d+) {
postgres_pass database;
postgres_escape $id;
postgres_query POST "UPDATE sms SET sms_recipt = '$request_body' WHERE id=$id RETURNING 'ACK'";
postgres_rewrite POST changes 200;
postgres_rewrite POST no_changes 410;
postgres_output value;
}

The variable `request_body` cannot be processed by the `postgres_escape` directive during the rewrite phase. Therefore, for security reasons, the `request_body` value that is used in `postgres_query` is always processing to escape string literals during the content phase without the need for `postgres_escape`.

This behavior can be disabled by directive `postgres_escape_request_body off`.

Testing
=======
`ngx_postgres` comes with complete test suite based on [Test::Nginx](http://github.com/agentzh/test-nginx).
134 changes: 133 additions & 1 deletion src/ngx_postgres_escape.c
Original file line number Diff line number Diff line change
@@ -31,10 +31,11 @@
#include "ngx_postgres_ddebug.h"
#include "ngx_postgres_escape.h"
#include "ngx_postgres_module.h"
#include "ngx_postgres_util.h"

#include <libpq-fe.h>


#define SINGLE_QUOTE_CHAR 39 /* ASCII code of Apostrophe = 39 */
uintptr_t ngx_postgres_script_exit_code = (uintptr_t) NULL;


@@ -95,3 +96,134 @@ ngx_postgres_escape_string(ngx_http_script_engine_t *e)
v->no_cacheable = 0;
v->not_found = 0;
}


/* Purpose: calculate new length (with doubled quotes) for variable value.
* This method returns number of the single qoute characters (apostrophes)
* plus original length of text value */
size_t
ngx_postgres_upstream_var_len_with_quotes(ngx_http_script_engine_t *e)
{
size_t i;
size_t result;
size_t count;
ngx_http_script_var_code_t *code;
ngx_http_variable_value_t *value;
ngx_http_core_main_conf_t *cmcf;
ngx_http_variable_t *v;
ngx_http_request_t *r;

dd("entering");
result = 0;
count = 0;

if ((e != NULL) && (e->ip != NULL) && (e->request != NULL)) {
code = (ngx_http_script_var_code_t *) e->ip;
e->ip += sizeof(ngx_http_script_var_code_t);

if (!e->skip) {
if (e->flushed) {
value = ngx_http_get_indexed_variable(e->request, code->index);

} else {
value = ngx_http_get_flushed_variable(e->request, code->index);
}

if ((value != NULL) && (!value->not_found)) {
r = e->request;
cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
v = cmcf->variables.elts;

if (v[code->index].get_handler ==
(ngx_http_get_variable_pt)ngx_postgres_rewrite_var) {
/* Quotes already escaped. Do nothing. */
return value->len;
}

for (i = 0; i < value->len; i++) {
if (value->data[i] == SINGLE_QUOTE_CHAR) {
count++;
}
}

result = value->len + count;
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, e->request->connection->log,
0, "http script: postgres: count to replace = %d", count);
}
}
}
dd("returning");

return result;
}


/* Purpose: replace quotes in variable values before substitute them in a query
* This method doubles the single qoute character (example: Can't -> Can''t) */
void
ngx_postgres_upstream_replace_quotes(ngx_http_script_engine_t *e)
{
int i;
int k;
u_char *p;
u_char *data;
ngx_http_script_var_code_t *code;
ngx_http_variable_value_t *value;
ngx_http_core_main_conf_t *cmcf;
ngx_http_variable_t *v;
ngx_http_request_t *r;

dd("entering");

if ((e != NULL) && (e->ip != NULL) && (e->request != NULL)) {
code = (ngx_http_script_var_code_t *) e->ip;

e->ip += sizeof(ngx_http_script_var_code_t);

if (!e->skip) {
if (e->flushed) {
value = ngx_http_get_indexed_variable(e->request, code->index);

} else {
value = ngx_http_get_flushed_variable(e->request, code->index);
}

if ((value != NULL) && (!value->not_found)
&& (e->buf.data != NULL)) {
p = e->pos;
data = value->data;

r = e->request;
cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
v = cmcf->variables.elts;

if (v[code->index].get_handler ==
(ngx_http_get_variable_pt) ngx_postgres_rewrite_var) {
/* Quotes already escaped. Do nothing. */
return;
}

ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"http script: postgres: replace quotes in $%V (\"%v\")",
&v[code->index].name, value);

/* copy var value like ngx_http_script_copy_var_code(e) does */
for (i = 0, k = 0; i < value->len; i++, k++) {
/* doubles all the qoute characters,
* do not double backslashes */
if (data[i] == SINGLE_QUOTE_CHAR) {
p[k] = data[i];
k++;
}

p[k] = data[i];
}

e->pos += k;
}
}
}

dd("returning");
}

5 changes: 3 additions & 2 deletions src/ngx_postgres_escape.h
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@
#include <ngx_http.h>


void ngx_postgres_escape_string(ngx_http_script_engine_t *);

void ngx_postgres_escape_string(ngx_http_script_engine_t *);
size_t ngx_postgres_upstream_var_len_with_quotes(ngx_http_script_engine_t *e);
void ngx_postgres_upstream_replace_quotes(ngx_http_script_engine_t *e);
#endif /* _NGX_POSTGRES_ESCAPE_H_ */
116 changes: 106 additions & 10 deletions src/ngx_postgres_handler.c
Original file line number Diff line number Diff line change
@@ -45,7 +45,6 @@ ngx_postgres_handler(ngx_http_request_t *r)
ngx_postgres_ctx_t *pgctx;
ngx_http_core_loc_conf_t *clcf;
ngx_http_upstream_t *u;
ngx_connection_t *c;
ngx_str_t host;
ngx_url_t url;
ngx_int_t rc;
@@ -82,12 +81,6 @@ ngx_postgres_handler(ngx_http_request_t *r)
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

rc = ngx_http_discard_request_body(r);
if (rc != NGX_OK) {
dd("returning rc:%d", (int) rc);
return rc;
}

#if defined(nginx_version) \
&& (((nginx_version >= 7063) && (nginx_version < 8000)) \
|| (nginx_version >= 8007))
@@ -209,8 +202,110 @@ ngx_postgres_handler(ngx_http_request_t *r)
r->main->count++;
#endif

ngx_http_upstream_init(r);
rc = (ngx_int_t) ngx_postgres_read_req_body(r);

if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"postgres: Where is a special response while reading request body. "
"Return code rc=%d", rc);
return rc;
}

return NGX_DONE;
}


ngx_int_t
ngx_postgres_read_req_body(ngx_http_request_t *r)
{
ngx_int_t rc;
ngx_postgres_ctx_t *ctx;
rc = NGX_OK;

if (r == NULL) {
/* postgres: Error. First argument (ngx_http_request_t *r) in function
ngx_postgres_read_req_body() can not be NULL (r = NULL) */
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

r->request_body_in_single_buf = 1;
r->request_body_in_persistent_file = 1;
r->request_body_in_clean_file = 1;

#if 1
if (r->request_body_in_file_only) {
r->request_body_file_log_level = 0;
}
#endif

ctx = ngx_http_get_module_ctx(r, ngx_postgres_module);
if (ctx == NULL) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"postgres: Error. Unable to get module context. "
"Method ngx_http_get_module_ctx(r, ngx_postgres_module) "
"returns ctx == NULL", rc);
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"postgres: start to read buffered request body");

rc = ngx_http_read_client_request_body(r, ngx_postgres_body_handler);

#if (nginx_version < 1002006) || \
(nginx_version >= 1003000 && nginx_version < 1003009)
r->main->count--;
#endif

if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"postgres: http read client request body returned error code %i", rc);

return NGX_DONE;
}

#if (nginx_version >= 1002006 && nginx_version < 1003000) || \
nginx_version >= 1003009
r->main->count--;
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"postgres: decrement r->main->count: %d", (int) r->main->count);
#endif

if (rc == NGX_AGAIN) {
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"postgres: read buffered request body requires I/O interruptions");

ctx->waiting_more_body = 1;
return NGX_AGAIN;
}

/* rc == NGX_OK */

ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"postgres: has read buffered request body in a single run");
return NGX_DONE;
}


void
ngx_postgres_body_handler(ngx_http_request_t *r)
{
ngx_postgres_ctx_t *ctx;
ngx_http_upstream_t *u;
ngx_connection_t *c;

ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"postgres: req body post read, c:%ud", r->main->count);

ctx = ngx_http_get_module_ctx(r, ngx_postgres_module);

if (ctx->waiting_more_body) {
ctx->waiting_more_body = 0;
r->read_event_handler = ngx_http_block_reading;
}

ngx_http_upstream_init(r);
u = r->upstream;
/* override the read/write event handler to our own */
u->write_event_handler = ngx_postgres_wev_handler;
u->read_event_handler = ngx_postgres_rev_handler;
@@ -236,14 +331,15 @@ ngx_postgres_handler(ngx_http_request_t *r)
#if defined(nginx_version) && (nginx_version >= 8017)
NGX_HTTP_SERVICE_UNAVAILABLE);
#else
pgctx->status ? pgctx->status : NGX_HTTP_INTERNAL_SERVER_ERROR);
ctx->status ? ctx->status : NGX_HTTP_INTERNAL_SERVER_ERROR);
#endif
}

dd("returning NGX_DONE");
return NGX_DONE;
return;
}


void
ngx_postgres_wev_handler(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
2 changes: 2 additions & 0 deletions src/ngx_postgres_handler.h
Original file line number Diff line number Diff line change
@@ -34,6 +34,8 @@


ngx_int_t ngx_postgres_handler(ngx_http_request_t *);
ngx_int_t ngx_postgres_read_req_body(ngx_http_request_t *r);
void ngx_postgres_body_handler(ngx_http_request_t *r);
void ngx_postgres_wev_handler(ngx_http_request_t *,
ngx_http_upstream_t *);
void ngx_postgres_rev_handler(ngx_http_request_t *,
223 changes: 223 additions & 0 deletions src/ngx_postgres_module.c
Original file line number Diff line number Diff line change
@@ -120,6 +120,20 @@ static ngx_command_t ngx_postgres_module_commands[] = {
offsetof(ngx_postgres_loc_conf_t, upstream.read_timeout),
NULL },

/* The flag add extra quote (as escape symbol) for each single quote symbol
* in the variable if the variable is substituted to the SQL.
* Setting <escape_request_body: off> disables the escape character
* substitution in the SQL string before single quotes.
* Disabling this option is pretty dangerous.
*
* default value: on */
{ ngx_string("postgres_escape_request_body"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_postgres_loc_conf_t, escape_request_body),
NULL },

ngx_null_command
};

@@ -228,6 +242,15 @@ ngx_postgres_output_enum_t ngx_postgres_output_handlers[] = {
{ ngx_null_string, 0, NULL }
};

static ngx_str_t request_body_var_name = ngx_string("request_body");

static ngx_int_t ngx_postgres_compile_with_quote_replace
(ngx_http_compile_complex_value_t *ccv);
static ngx_int_t ngx_postgres_add_quotes_escape_hook
(ngx_http_script_compile_t *sc);
static void ngx_postgres_replace_script_for_variable(ngx_array_t *array,
ngx_int_t var_index, ngx_http_script_code_pt old_value,
ngx_http_script_code_pt new_value);

ngx_int_t
ngx_postgres_add_variables(ngx_conf_t *cf)
@@ -321,6 +344,7 @@ ngx_postgres_create_loc_conf(ngx_conf_t *cf)
conf->rewrites = NGX_CONF_UNSET_PTR;
conf->output_handler = NGX_CONF_UNSET_PTR;
conf->variables = NGX_CONF_UNSET_PTR;
conf->escape_request_body = NGX_CONF_UNSET;

/* the hardcoded values */
conf->upstream.cyclic_temp_file = 0;
@@ -381,10 +405,22 @@ ngx_postgres_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)

ngx_conf_merge_ptr_value(conf->variables, prev->variables, NULL);

if (conf->escape_request_body == NGX_CONF_UNSET) {
if (prev->escape_request_body == NGX_CONF_UNSET) {
/* default value: on */
conf->escape_request_body = 1;

} else {
/* merge */
conf->escape_request_body = prev->escape_request_body;
}
}

dd("returning NGX_CONF_OK");
return NGX_CONF_OK;
}


/*
* Based on: ngx_http_upstream.c/ngx_http_upstream_server
* Copyright (C) Igor Sysoev
@@ -793,10 +829,25 @@ ngx_postgres_conf_query(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
ccv.value = &sql;
ccv.complex_value = query->cv;

if (pglcf->escape_request_body) {
/* Double the quote chars(') in values to prevent SQL injection */
/* Add extra function handlers in the variable treatment byte-code*/
if (ngx_postgres_compile_with_quote_replace(&ccv) != NGX_OK) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"postgres: error while compile script bytecode to quote "
"replace in \"%V\" \"%V\" \"%V\" directive. The mechanism "
"activated by flag <postgres_escape_request_body on> "
"works incorrectly.", &cmd->name, &value[1], &value[2]);
dd("returning NGX_CONF_ERROR");
return NGX_CONF_ERROR;
}

} else {
if (ngx_http_compile_complex_value(&ccv) != NGX_OK) {
dd("returning NGX_CONF_ERROR");
return NGX_CONF_ERROR;
}
}
} else {
/* simple value */
dd("simple value");
@@ -1336,3 +1387,175 @@ ngx_postgres_find_upstream(ngx_http_request_t *r, ngx_url_t *url)
dd("returning NULL");
return NULL;
}


/* Compile complex value with extra functions to double the quote chars(') */
static ngx_int_t
ngx_postgres_compile_with_quote_replace(ngx_http_compile_complex_value_t *ccv)
{
ngx_str_t *v;
ngx_uint_t i, n, nv, nc;
ngx_array_t flushes, lengths, values, *pf, *pl, *pv;
ngx_http_script_compile_t sc;

v = ccv->value;

nv = 0;
nc = 0;

for (i = 0; i < v->len; i++) {
if (v->data[i] == '$') {
if (v->data[i + 1] >= '1' && v->data[i + 1] <= '9') {
nc++;

} else {
nv++;
}
}
}

if ((v->len == 0 || v->data[0] != '$')
&& (ccv->conf_prefix || ccv->root_prefix))
{
if (ngx_conf_full_name(ccv->cf->cycle, v, ccv->conf_prefix) != NGX_OK) {
return NGX_ERROR;
}

ccv->conf_prefix = 0;
ccv->root_prefix = 0;
}

ccv->complex_value->value = *v;
ccv->complex_value->flushes = NULL;
ccv->complex_value->lengths = NULL;
ccv->complex_value->values = NULL;

if (nv == 0 && nc == 0) {
return NGX_OK;
}

n = nv + 1;

if (ngx_array_init(&flushes, ccv->cf->pool, n, sizeof(ngx_uint_t))
!= NGX_OK)
{
return NGX_ERROR;
}

n = nv * (2 * sizeof(ngx_http_script_copy_code_t)
+ sizeof(ngx_http_script_var_code_t))
+ sizeof(uintptr_t);

if (ngx_array_init(&lengths, ccv->cf->pool, n, 1) != NGX_OK) {
return NGX_ERROR;
}

n = (nv * (2 * sizeof(ngx_http_script_copy_code_t)
+ sizeof(ngx_http_script_var_code_t))
+ sizeof(uintptr_t)
+ v->len
+ sizeof(uintptr_t) - 1)
& ~(sizeof(uintptr_t) - 1);

if (ngx_array_init(&values, ccv->cf->pool, n, 1) != NGX_OK) {
return NGX_ERROR;
}

pf = &flushes;
pl = &lengths;
pv = &values;

ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));

sc.cf = ccv->cf;
sc.source = v;
sc.flushes = &pf;
sc.lengths = &pl;
sc.values = &pv;
sc.complete_lengths = 1;
sc.complete_values = 1;
sc.zero = ccv->zero;
sc.conf_prefix = ccv->conf_prefix;
sc.root_prefix = ccv->root_prefix;

if (ngx_http_script_compile(&sc) != NGX_OK) {
return NGX_ERROR;
}

if (ngx_postgres_add_quotes_escape_hook(&sc) != NGX_OK) {
return NGX_ERROR;
}

if (flushes.nelts) {
ccv->complex_value->flushes = flushes.elts;
ccv->complex_value->flushes[flushes.nelts] = (ngx_uint_t) -1;
}

ccv->complex_value->lengths = lengths.elts;
ccv->complex_value->values = values.elts;

return NGX_OK;
}


/*
* Add a hook to the script for copying variable values.
* The hook is needed to escape quotes in a variable request_body
* which gets value only during the content phase and
* cannot be handled by the postgres_escape directive during the rewite phase.
*
* Escaping quotes helps to prevent SQL injection
*/
static ngx_int_t
ngx_postgres_add_quotes_escape_hook(ngx_http_script_compile_t *sc)
{
ngx_int_t var_index;
if ((sc != NULL) && (sc->lengths != NULL) && (sc->values != NULL)) {

var_index = ngx_http_get_variable_index(sc->cf, &request_body_var_name);

/* Replace code pointers with functions for processing sql query
* variables in bytecode. Mounted functions can replace single quote
* characters in var text. */
ngx_postgres_replace_script_for_variable(*sc->lengths, var_index,
(ngx_http_script_code_pt)(void *)ngx_http_script_copy_var_len_code,
(ngx_http_script_code_pt)(void *)
ngx_postgres_upstream_var_len_with_quotes);
ngx_postgres_replace_script_for_variable(*sc->values, var_index,
(ngx_http_script_code_pt)(void *)ngx_http_script_copy_var_code,
(ngx_http_script_code_pt)(void *)
ngx_postgres_upstream_replace_quotes);
return NGX_OK;
}

else {
return NGX_ERROR;
}
}


static void
ngx_postgres_replace_script_for_variable(ngx_array_t *array,
ngx_int_t var_index, ngx_http_script_code_pt old_value,
ngx_http_script_code_pt new_value)
{
ngx_http_script_code_pt *cur;
ngx_http_script_code_pt *upper_bound;
ngx_http_script_code_pt code;
ngx_http_script_var_code_t *var_code;
size_t i;

upper_bound = (ngx_http_script_code_pt *) array->elts + array->nalloc;
cur = (ngx_http_script_code_pt *) array->elts;

for (i = 0; (ngx_uint_t)&cur[i] < (ngx_uint_t)upper_bound; i++) {
code = cur[i];
if (code == old_value) {
var_code = (ngx_http_script_var_code_t *)&cur[i];
if ((ngx_int_t) var_code->index == var_index)
{
cur[i] = new_value;
}
}
}
}
8 changes: 8 additions & 0 deletions src/ngx_postgres_module.h
Original file line number Diff line number Diff line change
@@ -176,6 +176,10 @@ typedef struct {
unsigned output_binary:1;
/* custom variables */
ngx_array_t *variables;
/* request body substitution mode */
/* on - adds escape symbols to quote symbols */
/* off - copy request body into sql with no changes */
ngx_flag_t escape_request_body;
} ngx_postgres_loc_conf_t;


@@ -187,6 +191,10 @@ typedef struct {
ngx_str_t var_query;
ngx_array_t *variables;
ngx_int_t status;
/* Flag to save state of request body recive
* true - waiting for more request body data;
* false - no need to wait */
ngx_flag_t waiting_more_body:1;
} ngx_postgres_ctx_t;


73 changes: 73 additions & 0 deletions t/request_body.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# vi:filetype=perl

use lib 'lib';
use Test::Nginx::Socket;

plan tests => repeat_each() * (2 + 2 + 1);

$ENV{TEST_NGINX_POSTGRESQL_HOST} ||= '127.0.0.1';
$ENV{TEST_NGINX_POSTGRESQL_PORT} ||= 5432;

our $http_config = <<'_EOC_';
upstream database {
postgres_server $TEST_NGINX_POSTGRESQL_HOST:$TEST_NGINX_POSTGRESQL_PORT
dbname=ngx_test user=ngx_test password=ngx_test;
}
_EOC_

no_shuffle();
run_tests();

__DATA__
=== TEST 1: capture request body
--- http_config eval: $::http_config
--- config
location /test {
postgres_pass database;
postgres_query POST "SELECT '$request_body'";
postgres_output value;
}
--- request eval
"POST /test
{ test: \"My simple request\" }"
--- error_code: 200
--- response_body eval
"{ test: \"My simple request\" }"
--- timeout: 10
=== TEST 2: escape request body
--- http_config eval: $::http_config
--- config
location /test {
postgres_pass database;
postgres_query POST "SELECT '$request_body'";
postgres_output value;
}
--- request eval
"POST /test
'; SQL injection attempt;"
--- error_code: 200
--- response_body eval
"'; SQL injection attempt;"
--- timeout: 10
=== TEST 3: escape request body disabled
--- http_config eval: $::http_config
--- config
location /test {
postgres_escape_request_body off;
postgres_pass database;
postgres_query POST "SELECT '$request_body'";
postgres_output value;
}
--- request eval
"POST /test
'; SQL injection attempt;--"
--- error_code: 500
--- timeout: 10