Skip to content

Commit 9ddb1c4

Browse files
committed
feat: nested macro expansion without recursion
1 parent 7db0379 commit 9ddb1c4

2 files changed

Lines changed: 159 additions & 104 deletions

File tree

apache2/re_actions.c

Lines changed: 137 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ int expand_macros(modsec_rec *msr, msc_string *var, msre_rule *rule, apr_pool_t
184184
char *text_start = NULL, *next_text_start = NULL;
185185
msc_string *part = NULL;
186186
int i, offset = 0;
187+
msc_string *tvar = NULL;
188+
msc_string *lasttvar = NULL;
187189

188190
if (var->value == NULL) return 0;
189191

@@ -192,130 +194,161 @@ int expand_macros(modsec_rec *msr, msc_string *var, msre_rule *rule, apr_pool_t
192194
* no macros in the input data.
193195
*/
194196

195-
data = apr_pstrdup(mptmp, var->value); /* IMP1 Are we modifying data anywhere? */
196-
arr = apr_array_make(mptmp, 16, sizeof(msc_string *));
197-
if ((data == NULL)||(arr == NULL)) return -1;
198-
199-
text_start = next_text_start = data;
197+
if (msr->txcfg->debuglog_level >= 9) {
198+
msr_log(msr, 9, "Received macro to resolve: %s", log_escape_nq_ex(mptmp, var->value, var->value_len));
199+
}
200+
data = apr_pstrndup(mptmp, var->value, var->value_len); /* IMP1 Are we modifying data anywhere? */
201+
text_start = data;
202+
// first loop - try to find all %{..} patterns as it exists
203+
// start with the innermost one
204+
int loopcnt = 1;
200205
do {
201-
text_start = next_text_start;
202-
p = strstr(text_start, "%");
203-
if (p != NULL) {
204-
char *var_name = NULL;
205-
char *var_value = NULL;
206-
207-
if ((*(p + 1) == '{')&&(*(p + 2) != '\0')) {
208-
char *var_start = p + 2;
209-
210-
t = var_start;
211-
while((*t != '\0')&&(*t != '}')) t++;
212-
if (*t == '}') {
213-
/* Named variable. */
214-
215-
var_name = apr_pstrmemdup(mptmp, var_start, t - var_start);
216-
q = strstr(var_name, ".");
217-
if (q != NULL) {
218-
var_value = q + 1;
219-
*q = '\0';
220-
}
221-
222-
next_text_start = t + 1; /* *t was '}' */
223-
} else {
224-
/* Warn about a possiblly forgotten '}' */
225-
if (msr->txcfg->debuglog_level >= 9) {
226-
msr_log(msr, 9, "Warning: Possibly unterminated macro: \"%s\"",
227-
log_escape_ex(mptmp, var_start - 2, t - var_start + 2));
228-
}
229-
230-
next_text_start = t; /* *t was '\0' */
206+
// try to find the last one '%{' seq
207+
char *last_open = NULL;
208+
char *first_close = NULL;
209+
while ((text_start = strstr(text_start, "%{")) != NULL) {
210+
last_open = text_start;
211+
text_start += 2; // move to next possible '%{'
212+
}
213+
// check if we found a macro open
214+
// if yes, find the first close char
215+
if (last_open != NULL) {
216+
first_close = strchr(last_open + 2, '}');
217+
if (first_close != NULL) {
218+
// we have the macro's position, split the string into three pieces
219+
// here_starts_the_var_%{tx.macro}_last_part
220+
// ^------------------^
221+
// ^---------^
222+
// ^--------^
223+
// create an array to store parts
224+
arr = apr_array_make(mptmp, 16, sizeof(msc_string *));
225+
226+
// create a variable from the macro
227+
char *var_name = NULL;
228+
char *var_value = NULL;
229+
var_name = apr_pstrmemdup(mptmp, last_open + 2, first_close - last_open - 2);
230+
q = strstr(var_name, ".");
231+
if (q != NULL) {
232+
var_value = q + 1;
233+
*q = '\0';
231234
}
232-
}
233235

234-
if (var_name != NULL) {
235-
char *my_error_msg = NULL;
236-
msre_var *var_generated = NULL;
237-
msre_var *var_resolved = NULL;
238-
239-
/* Add the text part before the macro to the array. */
240-
part = (msc_string *)apr_pcalloc(mptmp, sizeof(msc_string));
241-
if (part == NULL) return -1;
242-
part->value_len = p - text_start;
243-
part->value = apr_pstrmemdup(mptmp, text_start, part->value_len);
244-
*(msc_string **)apr_array_push(arr) = part;
245-
246-
/* Resolve the macro and add that to the array. */
247-
var_resolved = msre_create_var_ex(mptmp, msr->modsecurity->msre, var_name, var_value,
248-
msr, &my_error_msg);
249-
if (var_resolved != NULL) {
250-
var_generated = generate_single_var(msr, var_resolved, NULL, rule, mptmp);
251-
if (var_generated != NULL) {
252-
part = (msc_string *)apr_pcalloc(mptmp, sizeof(msc_string));
253-
if (part == NULL) return -1;
254-
part->value_len = var_generated->value_len;
255-
part->value = (char *)var_generated->value;
236+
// resolve the macro
237+
if (var_name != NULL) {
238+
char *my_error_msg = NULL;
239+
msre_var *var_generated = NULL;
240+
msre_var *var_resolved = NULL;
241+
242+
/* Add the text part before the macro to the array. */
243+
part = (msc_string *)apr_pcalloc(mptmp, sizeof(msc_string));
244+
if (part == NULL) return -1;
245+
part->value_len = last_open - data;
246+
if (part->value_len > 0) {
247+
part->value = apr_pstrmemdup(mptmp, data, part->value_len);
256248
*(msc_string **)apr_array_push(arr) = part;
257249
if (msr->txcfg->debuglog_level >= 9) {
250+
msr_log(msr, 9, "Macro's prefix in round #%d: %s", loopcnt, log_escape_nq_ex(mptmp, part->value, part->value_len));
251+
}
252+
}
253+
254+
/* Resolve the macro and add that to the array. */
255+
var_resolved = msre_create_var_ex(mptmp, msr->modsecurity->msre, var_name, var_value,
256+
msr, &my_error_msg);
257+
if (var_resolved != NULL) {
258+
var_generated = generate_single_var(msr, var_resolved, NULL, rule, mptmp);
259+
if (var_generated != NULL) {
260+
part = (msc_string *)apr_pcalloc(mptmp, sizeof(msc_string));
261+
if (part == NULL) return -1;
262+
part->value_len = var_generated->value_len;
263+
part->value = (char *)var_generated->value;
264+
*(msc_string **)apr_array_push(arr) = part;
258265
msr_log(msr, 9, "Resolved macro %%{%s%s%s} to: %s",
259266
var_name,
260267
(var_value ? "." : ""),
261268
(var_value ? var_value : ""),
262269
log_escape_nq_ex(mptmp, part->value, part->value_len));
263270
}
271+
} else {
272+
if (msr->txcfg->debuglog_level >= 4) {
273+
msr_log(msr, 4, "Failed to resolve macro %%{%s%s%s}: %s",
274+
var_name,
275+
(var_value ? "." : ""),
276+
(var_value ? var_value : ""),
277+
my_error_msg);
278+
}
264279
}
265280
} else {
266-
if (msr->txcfg->debuglog_level >= 4) {
267-
msr_log(msr, 4, "Failed to resolve macro %%{%s%s%s}: %s",
268-
var_name,
269-
(var_value ? "." : ""),
270-
(var_value ? var_value : ""),
271-
my_error_msg);
272-
}
281+
/* We could not identify a valid macro so add it as text.
282+
This part remained from previous implementation, probably it's not needed here,
283+
won't be executed ever */
284+
part = (msc_string *)apr_pcalloc(mptmp, sizeof(msc_string));
285+
if (part == NULL) return -1;
286+
part->value_len = last_open - data + 1; /* len(text)+len("%") */
287+
part->value = apr_pstrmemdup(mptmp, data, part->value_len);
288+
*(msc_string **)apr_array_push(arr) = part;
273289
}
274-
} else {
275-
/* We could not identify a valid macro so add it as text. */
290+
291+
// last part
276292
part = (msc_string *)apr_pcalloc(mptmp, sizeof(msc_string));
277-
if (part == NULL) return -1;
278-
part->value_len = p - text_start + 1; /* len(text)+len("%") */
279-
part->value = apr_pstrmemdup(mptmp, text_start, part->value_len);
280-
*(msc_string **)apr_array_push(arr) = part;
293+
part->value = apr_pstrdup(mptmp, first_close + 1);
294+
part->value_len = strlen(part->value);
295+
if (part->value_len > 0) {
296+
*(msc_string **)apr_array_push(arr) = part;
297+
if (msr->txcfg->debuglog_level >= 9) {
298+
msr_log(msr, 9, "Macro's suffix in round #%d: %s", loopcnt, log_escape_nq_ex(mptmp, part->value, part->value_len));
299+
}
300+
}
301+
302+
if (arr->nelts > 0) {
303+
tvar = apr_palloc(mptmp, sizeof(msc_string));
304+
memset(tvar, '\0', sizeof(msc_string));
305+
/* Figure out the required size for the string. */
306+
tvar->value_len = 0;
307+
for(i = 0; i < arr->nelts; i++) {
308+
part = ((msc_string **)arr->elts)[i];
309+
tvar->value_len += part->value_len;
310+
}
281311

282-
next_text_start = p + 1;
312+
/* Allocate the string. */
313+
tvar->value = apr_palloc(msr->mp, tvar->value_len + 1);
314+
if (tvar->value == NULL) return -1;
315+
316+
/* Combine the parts. */
317+
offset = 0;
318+
for(i = 0; i < arr->nelts; i++) {
319+
if (part->value_len > 0) {
320+
part = ((msc_string **)arr->elts)[i];
321+
memcpy((char *)(tvar->value + offset), part->value, part->value_len);
322+
offset += part->value_len;
323+
}
324+
}
325+
tvar->value[offset] = '\0';
326+
lasttvar = tvar;
327+
data = apr_pstrdup(mptmp, tvar->value);
328+
text_start = data;
329+
}
330+
}
331+
else { // no close '}', can't resolve macro
332+
msr_log(msr, 9, "Warning: Possibly unterminated macro: \"%s\"",
333+
log_escape_ex(mptmp, last_open, strlen(last_open)));
334+
text_start = NULL;
335+
return -1;
283336
}
284-
} else {
285-
/* Text part. */
286-
part = (msc_string *)apr_pcalloc(mptmp, sizeof(msc_string));
287-
part->value = apr_pstrdup(mptmp, text_start);
288-
part->value_len = strlen(part->value);
289-
*(msc_string **)apr_array_push(arr) = part;
290337
}
291-
} while (p != NULL);
338+
else {
339+
// no other macro was found, exit from the loop
340+
text_start = NULL;
341+
}
292342

293-
/* If there's more than one member of the array that
294-
* means there was at least one macro present. Combine
295-
* text parts into a single string now.
296-
*/
297-
if (arr->nelts > 1) {
298-
/* Figure out the required size for the string. */
299-
var->value_len = 0;
300-
for(i = 0; i < arr->nelts; i++) {
301-
part = ((msc_string **)arr->elts)[i];
302-
var->value_len += part->value_len;
303-
}
304-
305-
/* Allocate the string. */
306-
var->value = apr_palloc(msr->mp, var->value_len + 1);
307-
if (var->value == NULL) return -1;
308-
309-
/* Combine the parts. */
310-
offset = 0;
311-
for(i = 0; i < arr->nelts; i++) {
312-
part = ((msc_string **)arr->elts)[i];
313-
memcpy((char *)(var->value + offset), part->value, part->value_len);
314-
offset += part->value_len;
315-
}
316-
var->value[offset] = '\0';
317-
}
343+
} while (text_start != NULL);
318344

345+
if (lasttvar != NULL) {
346+
msr_log(msr, 9, "Resolved macro: '%s' to '%s'",
347+
var->value,
348+
log_escape_ex(mptmp, lasttvar->value, lasttvar->value_len));
349+
var->value_len = lasttvar->value_len;
350+
var->value = apr_pstrndup(msr->mp, lasttvar->value, lasttvar->value_len);
351+
}
319352
return 1;
320353
}
321354

tests/regression/rule/00-basics.t

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,25 @@
8989
GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?test=value",
9090
),
9191
},
92+
{
93+
type => "rule",
94+
comment => "SecRule (chain) with nested macro",
95+
conf => qq(
96+
SecRuleEngine On
97+
SecDebugLog $ENV{DEBUG_LOG}
98+
SecDebugLogLevel 9
99+
SecAction "id:10000,phase:1,t:none,setvar:tx.10001_counter=1,setvar:tx.10001_counter2=1"
100+
SecRule ARGS "\@rx (\\\d+)=(\\\d+)" "id:10001,phase:2,deny,nolog,t:none,capture,setvar:'tx.10001_%{tx.10001_counter}_lval=%{tx.1}',setvar:'tx.10001_%{tx.10001_counter}_rval=%{tx.2}',setvar:'tx.10001_counter=+1',chain"
101+
SecRule TX:/10001_\\\d+_lval/ "\@streq %{tx.10001_%{tx.10001_counter2}_rval}" "setvar:'tx.10001_counter2=+1'"
102+
),
103+
match_log => {
104+
error => [ qr/ModSecurity: /, 1 ],
105+
debug => [ qr/Set variable "tx.10001_1_rval" to "1"\..*Resolved macro \%\{tx.10001_counter\} to: 2.*Set variable "tx.10001_2_rval" to "1"/s, 1 ],
106+
},
107+
match_response => {
108+
status => qr/^403$/,
109+
},
110+
request => new HTTP::Request(
111+
GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?a=1=1&b=2=1",
112+
),
113+
},

0 commit comments

Comments
 (0)