Skip to content

Commit 71f8f71

Browse files
committed
rules: support quoted values with spaces
Rules containing a space in a dir, path, or exe value were split at the space. The truncated attribute could produce an unintended prefix match, so rule order appeared to override policy. Add a rule token lexer that removes double-quote delimiters and preserves glob escapes. Apply it to subject and object parsing, reject malformed quotes, and document quote, percent-escape, glob, CLI, and trust-file behavior. Add regression coverage for quoted dir, path, exe, sets, glob patterns, literal percent escapes, and parent-directory ordering.
1 parent 4da8016 commit 71f8f71

6 files changed

Lines changed: 349 additions & 7 deletions

File tree

ChangeLog

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Add filter support for Debian backend
99
- Add support for globbing in rules using explicit glob demarcation
1010
- Fix logging to syslog/journald to not deadlock on log rotation
11+
- Allow rules to express file/dir names with spaces using double quotes
1112

1213
1.6
1314
- Many internal changes

doc/fapolicyd-cli.8

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,15 @@ Dumps the trust db contents for inspection. This will print the original trust s
149149
.TP
150150
.B \-f, \-\-file add|delete|update [path]
151151
Manage the file trust database.
152+
.PP
153+
The path is a command-line argument, not a rule-file value. Use the shell's
154+
quoting to pass a path containing a space, for example:
155+
.nf
156+
.B fapolicyd-cli --file add "/tmp/first last/"
157+
.B fapolicyd-cli --file delete "/tmp/first last/"
158+
.fi
159+
The CLI receives the unquoted path. A literal \fB%20\fP in a command-line path
160+
is not decoded to a space.
152161
.RS
153162
.TP 12
154163
.B add

doc/fapolicyd.rules.5

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,34 @@ is still accepted)
162162
This option matches against the hash of the file being accessed. The accepted hash length follows the trust source, so RPM entries built with SHA512 store SHA512 digests while other sources may continue to provide SHA256. The hash in the rules should be all lowercase letters and do NOT start with 0x. Lowercase is the default output of sha256sum and sha512sum. The SHA256HASH keyword remains for compatibility but is deprecated; prefer FILE_HASH for new rules.
163163
.RE
164164

165+
.SH QUOTED VALUES
166+
Rule fields are separated by spaces. Enclose a complete string value in double
167+
quotes when it contains a space. The surrounding quotes are syntax and are not
168+
part of the value used for matching. This applies consistently to subject
169+
.B exe ,
170+
subject and object
171+
.B dir ,
172+
and object
173+
.BR path :
174+
175+
.nf
176+
.B allow perm=execute all : dir="/tmp/first last/"
177+
.B allow perm=execute exe="/tmp/first last/tool" : all
178+
.B allow perm=execute all : path="/tmp/first last/tool"
179+
.fi
180+
181+
Double quotes can also be used in named string-set definitions and in each
182+
comma-separated string value. Within a quoted value, a backslash before a
183+
double quote or another backslash represents that character. Other backslashes
184+
are preserved, so glob patterns retain their normal backslash behavior.
185+
186+
Percent escapes are not interpreted in rules. For example,
187+
.B dir="/tmp/first%20last/"
188+
matches a literal percent sign followed by \fB20\fP, not a space. A backslash
189+
before a space and shell-style single quotes are not rule-file quoting forms;
190+
double quotes are the only supported way to include a literal space in a rule
191+
value.
192+
165193
.SH PATH GLOBBING
166194
Rule values use exact matching by default. Prefix a value with
167195
.B glob:
@@ -223,6 +251,21 @@ pattern. Backslash quotes a metacharacter. Recursive
223251
.B **
224252
globstar matching is not supported.
225253

254+
When a glob value contains a space, quote the complete value, including the
255+
.B glob:
256+
prefix. For example:
257+
258+
.nf
259+
.B allow perm=execute all : path="glob:/tmp/first last/*"
260+
.B allow perm=execute exe="glob:/tmp/first last/*" : all
261+
.fi
262+
263+
The quotes are removed before glob validation and matching. The
264+
.B dir
265+
attribute remains a literal prefix and does not accept a quoted or unquoted
266+
.B glob:
267+
value.
268+
226269
Bracket ranges such as
227270
.B [a-z]
228271
and named character classes such as

doc/fapolicyd.trust.5

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,17 @@ fapolicyd.trust \- fapolicyd's file of trust
55
The file
66
.I /etc/fapolicyd/fapolicyd.trust
77
contains list of trusted files/binaries for the application whitelisting daemon. You may add comments to the file by starting the line with a '#' character.
8-
Each line has to contain three columns and space is a valid separator. The first column contains full path to the file, the second is size of the file in bytes
9-
and the third is valid sha256 hash.
8+
Each non-comment line contains a full path, size in bytes, and a valid SHA256
9+
hash. Version 3 trust files write paths without escaping and find the size and
10+
hash by splitting at the final two spaces. A path may therefore contain spaces.
11+
The \fBfapolicyd-cli --file\fP commands receive ordinary shell arguments; quote
12+
a path containing a space in the shell, for example
13+
\fBfapolicyd-cli --file add "/tmp/first last/"\fP. In a version 3 trust file,
14+
\fB%20\fP is a literal percent sign followed by \fB20\fP, not a space.
15+
.sp
16+
Version 2 trust files are accepted for compatibility and use their legacy
17+
percent escaping, including \fB%20\fP for a space. Newly rewritten trust files
18+
use version 3, so do not use version-2 percent escapes in new entries.
1019
.sp
1120
The directory \fI/etc/fapolicyd/trust\&.d\fR can be used to store multiple trust files\&.
1221
This way a privileged user can split the trust database into multiple files and manage them separately through \fBfapolicyd\-cli\fR\&.

src/library/rules.c

Lines changed: 121 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,106 @@ static int assign_subject(llist *l, lnode *n, int type,
7272
static int assign_object(llist *l, lnode *n, int type,
7373
const char *ptr2, int lineno) __wur;
7474

75+
enum rule_token_error {
76+
RULE_TOKEN_OK = 0,
77+
RULE_TOKEN_UNTERMINATED_QUOTE,
78+
RULE_TOKEN_TRAILING_TEXT
79+
};
80+
81+
/*
82+
* next_rule_token - get one space-delimited rule token with quoted values.
83+
* @cursor: current mutable position in the rule line, advanced on success.
84+
* @error: receives a quoted-value syntax error, if one occurs.
85+
*
86+
* A double quote begins quoted text only at the start of an attribute value
87+
* (immediately after '=' or ','). This preserves literal quotes elsewhere in
88+
* existing unquoted values. Quotes are removed in place, and only '\\"' and
89+
* '\\\\' are unescaped inside quoted text so fnmatch backslash escapes such as
90+
* '\\*' reach glob evaluation unchanged.
91+
*
92+
* Returns: the next token, or NULL when no token remains or @error is set.
93+
*/
94+
static char *next_rule_token(char **cursor, enum rule_token_error *error)
95+
{
96+
char *read, *write, *token;
97+
bool quoted = false;
98+
99+
if (error)
100+
*error = RULE_TOKEN_OK;
101+
if (!cursor || !*cursor)
102+
return NULL;
103+
104+
read = *cursor;
105+
while (*read == ' ')
106+
read++;
107+
if (*read == '\0') {
108+
*cursor = read;
109+
return NULL;
110+
}
111+
112+
token = read;
113+
write = read;
114+
while (*read) {
115+
if (quoted) {
116+
if (*read == '\\' &&
117+
(read[1] == '"' || read[1] == '\\')) {
118+
*write++ = read[1];
119+
read += 2;
120+
continue;
121+
}
122+
if (*read == '"') {
123+
quoted = false;
124+
read++;
125+
if (*read && *read != ' ' && *read != ',') {
126+
if (error)
127+
*error = RULE_TOKEN_TRAILING_TEXT;
128+
return NULL;
129+
}
130+
continue;
131+
}
132+
*write++ = *read++;
133+
continue;
134+
}
135+
136+
if (*read == ' ')
137+
break;
138+
if (*read == '"' && read > token &&
139+
(read[-1] == '=' || read[-1] == ',')) {
140+
quoted = true;
141+
read++;
142+
continue;
143+
}
144+
*write++ = *read++;
145+
}
146+
147+
if (quoted) {
148+
if (error)
149+
*error = RULE_TOKEN_UNTERMINATED_QUOTE;
150+
return NULL;
151+
}
152+
153+
while (*read == ' ')
154+
read++;
155+
*write = '\0';
156+
*cursor = read;
157+
return token;
158+
}
159+
160+
/*
161+
* report_rule_token_error - emit an actionable rule-value lexer diagnostic.
162+
* @error: quoted-value error reported by next_rule_token().
163+
* @lineno: source line used for the diagnostic.
164+
*
165+
* Returns: none.
166+
*/
167+
static void report_rule_token_error(enum rule_token_error error, int lineno)
168+
{
169+
if (error == RULE_TOKEN_UNTERMINATED_QUOTE)
170+
msg(LOG_ERR, "Unterminated quoted value in line %d", lineno);
171+
else if (error == RULE_TOKEN_TRAILING_TEXT)
172+
msg(LOG_ERR, "Characters after quoted value in line %d", lineno);
173+
}
174+
75175
/*
76176
* validate_glob_set - validate explicitly marked glob values for an attribute
77177
* @type: parsed subject or object attribute type.
@@ -822,12 +922,14 @@ static int assign_object(llist *l, lnode *n, int type,
822922
}
823923

824924

825-
static int parse_new_format(llist *l, lnode *n, int lineno)
925+
static int parse_new_format(llist *l, lnode *n, int lineno,
926+
char **cursor)
826927
{
827928
int state = 0; // 0 == subj, 1 == obj
828929
char *ptr;
930+
enum rule_token_error token_error;
829931

830-
while ((ptr = strtok(NULL, " "))) {
932+
while ((ptr = next_rule_token(cursor, &token_error))) {
831933
int type;
832934
char *ptr2 = strchr(ptr, '=');
833935

@@ -873,6 +975,10 @@ static int parse_new_format(llist *l, lnode *n, int lineno)
873975
return 5;
874976
}
875977
}
978+
if (token_error != RULE_TOKEN_OK) {
979+
report_rule_token_error(token_error, lineno);
980+
return 5;
981+
}
876982
return 0;
877983
}
878984

@@ -1044,14 +1150,20 @@ static enum rule_parse_result nv_split(llist *l, char *buf, lnode *n,
10441150
int lineno)
10451151
{
10461152
char *ptr, *ptr2;
1153+
char *cursor = buf;
10471154
rformat_t format = RULE_FMT_ORIG;
10481155
attr_sets_t *sets = l->sets;
1156+
enum rule_token_error token_error;
10491157

10501158
if (strchr(buf, ':'))
10511159
format = RULE_FMT_COLON;
10521160
n->format = format;
10531161

1054-
ptr = strtok(buf, " ");
1162+
ptr = next_rule_token(&cursor, &token_error);
1163+
if (token_error != RULE_TOKEN_OK) {
1164+
report_rule_token_error(token_error, lineno);
1165+
return RULE_PARSE_ERROR;
1166+
}
10551167
if (ptr == NULL)
10561168
return RULE_PARSE_SKIP;
10571169
if (ptr[0] == '#')
@@ -1073,7 +1185,7 @@ static enum rule_parse_result nv_split(llist *l, char *buf, lnode *n,
10731185
// Default access permission is open
10741186
n->a = OPEN_ACC;
10751187

1076-
while ((ptr = strtok(NULL, " "))) {
1188+
while ((ptr = next_rule_token(&cursor, &token_error))) {
10771189
int type;
10781190

10791191
ptr2 = strchr(ptr, '=');
@@ -1104,7 +1216,7 @@ static enum rule_parse_result nv_split(llist *l, char *buf, lnode *n,
11041216
lineno))
11051217
return RULE_PARSE_ERROR;
11061218
}
1107-
if (parse_new_format(l, n, lineno))
1219+
if (parse_new_format(l, n, lineno, &cursor))
11081220
return RULE_PARSE_ERROR;
11091221
goto finish_up;
11101222
}
@@ -1141,6 +1253,10 @@ static enum rule_parse_result nv_split(llist *l, char *buf, lnode *n,
11411253
return RULE_PARSE_ERROR;
11421254
}
11431255
}
1256+
if (token_error != RULE_TOKEN_OK) {
1257+
report_rule_token_error(token_error, lineno);
1258+
return RULE_PARSE_ERROR;
1259+
}
11441260

11451261
finish_up:
11461262
// do one last sanity check for missing subj or obj

0 commit comments

Comments
 (0)