Skip to content

Commit 9d2e909

Browse files
committed
initial implementation of safe variables
1 parent 4a97725 commit 9d2e909

File tree

6 files changed

+102
-27
lines changed

6 files changed

+102
-27
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,13 @@ should be redesigned or plpgsql_check should be disabled for this function.
297297
<i>A usage of plpgsql_check adds a small overhead (in enabled passive mode) and you should use
298298
it only in develop or preprod environments.</i>
299299

300+
<aside class="warning">
301+
Attention: The SQL injection check can detect only some SQL injection vulnerabilities. This tool
302+
cannot be used for security audit! Some issues should not be detected. This check can raise false
303+
alarms too - probably when variable is sanitized by other command or when value is of some compose
304+
type.
305+
</aside>
306+
300307
## Dynamic SQL
301308

302309
This module doesn't check queries that are assembled in runtime. It is not possible

src/check_expr.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,28 @@ plpgsql_check_expr_as_rvalue(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr,
757757
tupdesc = plpgsql_check_expr_get_desc(cstate, expr, use_element_type, expand, is_expression, &first_level_typoid);
758758
is_immutable_null = is_const_null_expr(cstate, expr);
759759

760+
/* try to detect safe variables */
761+
if (cstate->cinfo->security_warnings && targetdno != -1)
762+
{
763+
PLpgSQL_var *var = (PLpgSQL_var *) cstate->estate->datums[targetdno];
764+
765+
if (var->dtype == PLPGSQL_DTYPE_VAR)
766+
{
767+
bool typispreferred;
768+
char typcategory;
769+
770+
get_type_category_preferred(var->datatype->typoid, &typcategory, &typispreferred);
771+
if (typcategory == 'S')
772+
{
773+
Node *node = plpgsql_check_expr_get_node(cstate, expr, false);
774+
int location;
775+
776+
if (!plpgsql_check_is_sql_injection_vulnerable(cstate, expr, node, &location))
777+
cstate->safe_variables = bms_add_member(cstate->safe_variables, targetdno);
778+
}
779+
}
780+
}
781+
760782
if (expected_typoid != InvalidOid && type_is_rowtype(expected_typoid) && first_level_typoid != InvalidOid)
761783
{
762784
/* simple error, scalar source to composite target */

src/check_function.c

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1009,7 +1009,6 @@ plpgsql_check_setup_cstate(PLpgSQL_checkstate *cstate,
10091009
cstate->fake_rtd = fake_rtd;
10101010

10111011
cstate->safe_variables = NULL;
1012-
cstate->string_variables = NULL;
10131012

10141013
cstate->stop_check = false;
10151014
}

src/expr_walk.c

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
typedef struct
2222
{
2323
PLpgSQL_checkstate *cstate;
24+
PLpgSQL_expr *expr;
2425
char *query_str;
2526
} check_funcexpr_walker_params;
2627

@@ -29,7 +30,8 @@ static int check_fmt_string(const char *fmt,
2930
int location,
3031
check_funcexpr_walker_params *wp,
3132
bool *is_error,
32-
int *unsafe_expr_location);
33+
int *unsafe_expr_location,
34+
bool no_error);
3335

3436
/*
3537
* Send to ouput all not yet displayed relations and functions.
@@ -223,7 +225,7 @@ check_funcexpr_walker(Node *node, void *context)
223225

224226
wp = (check_funcexpr_walker_params *) context;
225227

226-
required_nargs = check_fmt_string(fmt, fexpr->args, c->location, wp, &is_error, NULL);
228+
required_nargs = check_fmt_string(fmt, fexpr->args, c->location, wp, &is_error, NULL, false);
227229
if (!is_error && required_nargs != -1)
228230
{
229231
if (required_nargs + 1 != list_length(fexpr->args))
@@ -251,6 +253,7 @@ plpgsql_check_funcexpr(PLpgSQL_checkstate *cstate, Query *query, char *query_str
251253

252254
wp.cstate = cstate;
253255
wp.query_str = query_str;
256+
wp.expr = NULL;
254257

255258
check_funcexpr_walker((Node *) query, &wp);
256259
}
@@ -480,15 +483,17 @@ text_format_parse_format(const char *start_ptr,
480483
}
481484

482485
/*
483-
* Returns number of rquired arguments or -1 when we cannot detect this number
486+
* Returns number of rquired arguments or -1 when we cannot detect this number.
487+
* When no_error is true, then this function doesn't raise a error or warning.
484488
*/
485489
static int
486490
check_fmt_string(const char *fmt,
487491
List *args,
488492
int location,
489493
check_funcexpr_walker_params *wp,
490494
bool *is_error,
491-
int *unsafe_expr_location)
495+
int *unsafe_expr_location,
496+
bool no_error)
492497
{
493498
const char *cp;
494499
const char *end_ptr = fmt + strlen(fmt);
@@ -535,7 +540,7 @@ check_fmt_string(const char *fmt,
535540
appendStringInfo(&sinfo,
536541
"unrecognized format() type specifier \"%c\"", *cp);
537542

538-
if (wp)
543+
if (!no_error)
539544
plpgsql_check_put_error(wp->cstate,
540545
ERRCODE_INVALID_PARAMETER_VALUE, 0,
541546
sinfo.data,
@@ -578,7 +583,7 @@ check_fmt_string(const char *fmt,
578583
{
579584
Node *arg = list_nth(args, argn - 1);
580585

581-
if (plpgsql_check_is_sql_injection_vulnerable(arg, unsafe_expr_location))
586+
if (plpgsql_check_is_sql_injection_vulnerable(wp->cstate, wp->expr, arg, unsafe_expr_location))
582587
{
583588
/* found vulnerability, stop */
584589
*is_error = false;
@@ -740,10 +745,13 @@ plpgsql_check_qual_has_fishy_cast(PlannedStmt *plannedstmt, Plan *plan, Param **
740745

741746
/*
742747
* Recursive iterate to deep and search extern params with typcategory "S", and check
743-
* if this value is sanitized.
748+
* if this value is sanitized. Flag is_safe is true, when result is safe.
744749
*/
745750
bool
746-
plpgsql_check_is_sql_injection_vulnerable(Node *node, int *location)
751+
plpgsql_check_is_sql_injection_vulnerable(PLpgSQL_checkstate *cstate,
752+
PLpgSQL_expr *expr,
753+
Node *node,
754+
int *location)
747755
{
748756
if (IsA(node, FuncExpr))
749757
{
@@ -755,7 +763,7 @@ plpgsql_check_is_sql_injection_vulnerable(Node *node, int *location)
755763
{
756764
Node *arg = lfirst(lc);
757765

758-
if (plpgsql_check_is_sql_injection_vulnerable(arg, location))
766+
if (plpgsql_check_is_sql_injection_vulnerable(cstate, expr, arg, location))
759767
{
760768
is_vulnerable = true;
761769
break;
@@ -793,10 +801,15 @@ plpgsql_check_is_sql_injection_vulnerable(Node *node, int *location)
793801
if (c->consttype == TEXTOID && !c->constisnull)
794802
{
795803
char *fmt = TextDatumGetCString(c->constvalue);
804+
check_funcexpr_walker_params wp;
796805
bool is_error;
797806

807+
wp.cstate = cstate;
808+
wp.expr = expr;
809+
wp.query_str = expr->query;
810+
798811
*location = -1;
799-
check_fmt_string(fmt, fexpr->args, c->location, NULL, &is_error, location);
812+
check_fmt_string(fmt, fexpr->args, c->location, &wp, &is_error, location, true);
800813

801814
/* only in this case, "format" function obviously sanitize parameters */
802815
if (!is_error)
@@ -826,7 +839,7 @@ plpgsql_check_is_sql_injection_vulnerable(Node *node, int *location)
826839
{
827840
Node *arg = lfirst(lc);
828841

829-
if (plpgsql_check_is_sql_injection_vulnerable(arg, location))
842+
if (plpgsql_check_is_sql_injection_vulnerable(cstate, expr, arg, location))
830843
{
831844
is_vulnerable = true;
832845
break;
@@ -861,24 +874,48 @@ plpgsql_check_is_sql_injection_vulnerable(Node *node, int *location)
861874
}
862875
else if (IsA(node, NamedArgExpr))
863876
{
864-
return plpgsql_check_is_sql_injection_vulnerable((Node *) ((NamedArgExpr *) node)->arg, location);
877+
return plpgsql_check_is_sql_injection_vulnerable(cstate, expr, (Node *) ((NamedArgExpr *) node)->arg, location);
865878
}
866879
else if (IsA(node, RelabelType))
867880
{
868-
return plpgsql_check_is_sql_injection_vulnerable((Node *) ((RelabelType *) node)->arg, location);
881+
return plpgsql_check_is_sql_injection_vulnerable(cstate, expr, (Node *) ((RelabelType *) node)->arg, location);
869882
}
870883
else if (IsA(node, Param))
871884
{
872885
Param *p = (Param *) node;
873886

874-
if (p->paramkind == PARAM_EXTERN)
887+
if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_EXEC)
875888
{
876889
bool typispreferred;
877890
char typcategory;
878891

879892
get_type_category_preferred(p->paramtype, &typcategory, &typispreferred);
880893
if (typcategory == 'S')
881894
{
895+
if (p->paramkind == PARAM_EXTERN && p->paramid > 0 && p->location != -1)
896+
{
897+
int dno = p->paramid - 1;
898+
899+
/*
900+
* When paramid looks well and related datum is variable with same
901+
* type, then we can check, if this variable has sanitized content
902+
* already.
903+
*/
904+
if (expr && bms_is_member(dno, expr->paramnos))
905+
{
906+
PLpgSQL_var *var = (PLpgSQL_var *) cstate->estate->datums[dno];
907+
908+
if (var->dtype == PLPGSQL_DTYPE_VAR)
909+
{
910+
if (var->datatype->typoid == p->paramtype)
911+
{
912+
if (bms_is_member(dno, cstate->safe_variables))
913+
return false;
914+
}
915+
}
916+
}
917+
}
918+
882919
*location = p->location;
883920
return true;
884921
}

src/plpgsql_check.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ typedef struct PLpgSQL_checkstate
102102
plpgsql_check_result_info *result_info;
103103
plpgsql_check_info *cinfo;
104104
Bitmapset *safe_variables; /* track which variables are safe against sql injection */
105-
Bitmapset *string_variables; /* track which variables are possibly vulnerable to sql injection */
106105
bool stop_check; /* true after error when fatal_errors option is active */
107106
} PLpgSQL_checkstate;
108107

@@ -181,7 +180,7 @@ extern void plpgsql_check_detect_dependency(PLpgSQL_checkstate *cstate, Query *q
181180
extern bool plpgsql_check_has_rtable(Query *query);
182181
extern bool plpgsql_check_qual_has_fishy_cast(PlannedStmt *plannedstmt, Plan *plan, Param **param);
183182
extern void plpgsql_check_funcexpr(PLpgSQL_checkstate *cstate, Query *query, char *query_str);
184-
extern bool plpgsql_check_is_sql_injection_vulnerable(Node *node, int *location);
183+
extern bool plpgsql_check_is_sql_injection_vulnerable(PLpgSQL_checkstate *cstate, PLpgSQL_expr *expr, Node *node, int *location);
185184

186185
/*
187186
* functions from check_expr.c

src/stmtwalk.c

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1892,17 +1892,28 @@ check_dynamic_sql(PLpgSQL_checkstate *cstate,
18921892
* but we can check sanitize parameters.
18931893
*/
18941894
if (cstate->cinfo->security_warnings &&
1895-
plpgsql_check_is_sql_injection_vulnerable(expr_node, &loc))
1895+
plpgsql_check_is_sql_injection_vulnerable(cstate, query, expr_node, &loc))
18961896
{
1897-
plpgsql_check_put_error(cstate,
1898-
0, 0,
1899-
"text type variable is not sanitized",
1900-
"The EXECUTE expression is SQL injection vulnerable.",
1901-
"Use quote_ident, quote_literal or format function to secure variable.",
1902-
PLPGSQL_CHECK_WARNING_SECURITY,
1903-
loc,
1904-
query->query,
1905-
NULL);
1897+
if (loc != -1)
1898+
plpgsql_check_put_error(cstate,
1899+
0, 0,
1900+
"text type variable is not sanitized",
1901+
"The EXECUTE expression is SQL injection vulnerable.",
1902+
"Use quote_ident, quote_literal or format function to secure variable.",
1903+
PLPGSQL_CHECK_WARNING_SECURITY,
1904+
loc,
1905+
query->query,
1906+
NULL);
1907+
else
1908+
plpgsql_check_put_error(cstate,
1909+
0, 0,
1910+
"the expression is not SQL injection safe",
1911+
"Cannot ensure so dynamic EXECUTE statement is SQL injection secure.",
1912+
"Use quote_ident, quote_literal or format function to secure variable.",
1913+
PLPGSQL_CHECK_WARNING_SECURITY,
1914+
-1,
1915+
query->query,
1916+
NULL);
19061917
}
19071918

19081919
/*

0 commit comments

Comments
 (0)