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
Show file tree
Hide file tree
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
Expand Up @@ -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`
Expand Down Expand Up @@ -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).
Expand Down
134 changes: 133 additions & 1 deletion src/ngx_postgres_escape.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;


Expand Down Expand Up @@ -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
Expand Up @@ -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
Expand Up @@ -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;
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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;
Expand All @@ -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)
{
Expand Down
2 changes: 2 additions & 0 deletions src/ngx_postgres_handler.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 *,
Expand Down
Loading