From 795711d07775352a5ccd3318bad47a6b6da794ce Mon Sep 17 00:00:00 2001 From: MarkLee131 Date: Wed, 29 Apr 2026 22:37:37 +0800 Subject: [PATCH] parser: reject [ and ] in %option= values to prevent m4-quote injection The OPTION-state quoted-string rule at src/scan.l:480 accepts arbitrary non-quote, non-newline bytes, including [ and ]. Those values are later interpolated into m4 input as `m4_define([[NAME]],[[%s]])` in filter.c and main.c. A `]]` inside the value breaks out of m4's quoting and lets a .l file run arbitrary m4 builtins (m4_syscmd / m4_esyscmd / m4_include) during `flex foo.l`. PR #81 (7528bc0, 2016) added ESCAPED_QSTART / ESCAPED_QEND escaping for action and code-block contexts, but the same fix was never extended to %option= values. Only `prefix=` had a brackets-rejection guard. Apply the same guard to all eight %option= values that flow into m4: outfile, header-file, yydecl, yyclass, extra-type, pre-action, post-action, yyterminate. (prefix already had the check; the helper replaces the inline one.) Verified each value rejects [ and ] with a clear error message, and a normal .l file still builds. --- src/flexdef.h | 4 ++++ src/misc.c | 24 ++++++++++++++++++++++++ src/parse.y | 27 +++++++++++++++++---------- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/flexdef.h b/src/flexdef.h index adf4fd0a7..2f2f93cc1 100644 --- a/src/flexdef.h +++ b/src/flexdef.h @@ -849,6 +849,10 @@ extern void dataflush(void); /* Report an error message and terminate. */ extern void flexerror(const char *); +/* Reject `[` / `]` in a %option= value to prevent m4-quote injection. */ +extern void reject_brackets_in_option_value(const char *opt_name, + const char *val); + /* Report a fatal error message and terminate. */ extern void flexfatal(const char *); diff --git a/src/misc.c b/src/misc.c index 56d2abab6..39b5e34f3 100644 --- a/src/misc.c +++ b/src/misc.c @@ -205,6 +205,30 @@ void flexerror (const char *msg) } +/* reject_brackets_in_option_value - bail out if a %option= value contains + * `[` or `]`. Such characters break out of the m4 quoting scheme that + * flex uses to interpolate option values into m4 input (see PR #81 which + * added the same escaping for action / code-block contexts via + * ESCAPED_QSTART / ESCAPED_QEND in src/scan.l). The OPTION-state + * quoted-string rule at src/scan.l:480 was never plumbed through that + * escape, so without this guard a malicious .l file can inject arbitrary + * m4 (e.g. m4_syscmd) into the output pipeline. + * + * Mirrors the existing prefix= guard that has been in parse.y since + * before PR #81. + */ +void reject_brackets_in_option_value (const char *opt_name, const char *val) +{ + if (val != NULL && (strchr (val, '[') || strchr (val, ']'))) { + char msg[MAXLINE]; + snprintf (msg, sizeof msg, + _("%%option %s value must not contain '[' or ']'"), + opt_name); + flexerror (msg); + } +} + + /* flexfatal - report a fatal error message and terminate */ void flexfatal (const char *msg) diff --git a/src/parse.y b/src/parse.y index 80e7f0977..830002938 100644 --- a/src/parse.y +++ b/src/parse.y @@ -199,27 +199,33 @@ optionlist : optionlist option option : TOK_OUTFILE '=' NAME { + reject_brackets_in_option_value("outfile", nmstr); env.outfilename = xstrdup(nmstr); env.did_outfilename = 1; } | TOK_EXTRA_TYPE '=' NAME - { extra_type = xstrdup(nmstr); } + { reject_brackets_in_option_value("extra-type", nmstr); + extra_type = xstrdup(nmstr); } | TOK_PREFIX '=' NAME - { ctrl.prefix = xstrdup(nmstr); - if (strchr(ctrl.prefix, '[') || strchr(ctrl.prefix, ']')) - flexerror(_("Prefix must not contain [ or ]")); } + { reject_brackets_in_option_value("prefix", nmstr); + ctrl.prefix = xstrdup(nmstr); } | TOK_YYCLASS '=' NAME - { ctrl.yyclass = xstrdup(nmstr); } + { reject_brackets_in_option_value("yyclass", nmstr); + ctrl.yyclass = xstrdup(nmstr); } | TOK_HEADER_FILE '=' NAME - { env.headerfilename = xstrdup(nmstr); } + { reject_brackets_in_option_value("header-file", nmstr); + env.headerfilename = xstrdup(nmstr); } | TOK_YYLMAX '=' TOK_NUMERIC { ctrl.yylmax = nmval; } | TOK_YYDECL '=' NAME - { ctrl.yydecl = xstrdup(nmstr); } + { reject_brackets_in_option_value("yydecl", nmstr); + ctrl.yydecl = xstrdup(nmstr); } | TOK_PREACTION '=' NAME - { ctrl.preaction = xstrdup(nmstr); } + { reject_brackets_in_option_value("pre-action", nmstr); + ctrl.preaction = xstrdup(nmstr); } | TOK_POSTACTION '=' NAME - { ctrl.postaction = xstrdup(nmstr); } + { reject_brackets_in_option_value("post-action", nmstr); + ctrl.postaction = xstrdup(nmstr); } | TOK_BUFSIZE '=' TOK_NUMERIC { ctrl.bufsize = nmval; } | TOK_EMIT '=' NAME @@ -227,7 +233,8 @@ option : TOK_OUTFILE '=' NAME | TOK_USERINIT '=' NAME { ctrl.userinit = xstrdup(nmstr); } | TOK_YYTERMINATE '=' NAME - { ctrl.yyterminate = xstrdup(nmstr); } + { reject_brackets_in_option_value("yyterminate", nmstr); + ctrl.yyterminate = xstrdup(nmstr); } | TOK_TABLES_FILE '=' NAME { tablesext = true; tablesfilename = xstrdup(nmstr); } ;