@@ -278,26 +278,111 @@ function _wildcard_limit() {
278278 return true ; // admin may always add wildcard domain
279279 }
280280
281+ /**
282+ * Parses $expression to check if it is a valid shell glob pattern.
283+ * Does not support extended glob matching syntax.
284+ *
285+ * @see https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html
286+ * @param string $expression
287+ * @param string $allowed_chars regexp of allowed characters in expression. ? and * are always allowed
288+ * @param string|null $allowed_brace_chars regexp of allowed characters in brace ([...]). Dash is always allowed. If empty, then $allowed_chars will be used
289+ * @return bool
290+ */
291+ private function validate_glob ($ expression , $ allowed_chars = '/^.$/u ' , $ allowed_brace_chars = null ) {
292+ $ escaping = false ;
293+ $ in_brace = false ;
294+ $ brace_content = [];
295+ $ chars = preg_split ('//u ' , $ expression , -1 , PREG_SPLIT_NO_EMPTY );
296+ foreach ($ chars as $ i => $ c ) {
297+ if ($ in_brace ) {
298+ // the first char after brace start can be a ].
299+ if (($ c == '] ' && empty ($ brace_content )) || $ c != '] ' ) {
300+ $ brace_content [] = $ c ;
301+ } else {
302+ $ in_brace = false ;
303+ $ last_is_dash = false ;
304+ foreach ($ brace_content as $ bi => $ bc ) {
305+ // dashes are always allowed
306+ if ($ bc == '- ' ) {
307+ // ... but we consider consecutive dashes as invalid
308+ if ($ last_is_dash ) {
309+ return false ;
310+ }
311+ // ... and need to validate it as allowed char when it is first or last
312+ if (($ bi == 0 || $ bi == count ($ brace_content ) - 1 ) && !preg_match ($ allowed_brace_chars ?: $ allowed_chars , '- ' )) {
313+ return false ;
314+ }
315+ $ last_is_dash = true ;
316+ } else {
317+ $ last_is_dash = false ;
318+ // negate chars are always allowed
319+ if ($ bi == 0 && ($ bc == '^ ' || $ bc == '! ' ) && count ($ brace_content ) > 1 ) {
320+ continue ;
321+ }
322+ if (!preg_match ($ allowed_brace_chars ?: $ allowed_chars , $ bc )) {
323+ return false ;
324+ }
325+ }
326+ }
327+ }
328+ } else {
329+ $ peek = $ i == count ($ chars ) - 1 ? '' : $ chars [$ i + 1 ];
330+ if ($ c == '\\' && in_array ($ peek , ['[ ' , '] ' , '* ' , '? ' ])) {
331+ $ escaping = true ;
332+ continue ;
333+ } elseif ($ c == '[ ' && !$ escaping ) {
334+ $ in_brace = true ;
335+ $ brace_content = [];
336+ } elseif ($ escaping || ($ c != '? ' && $ c != '* ' )) {
337+ if (!preg_match ($ allowed_chars , $ c )) {
338+ return false ;
339+ }
340+ }
341+ $ escaping = false ;
342+ }
343+ }
344+ return !$ in_brace && !$ escaping ;
345+ }
346+
281347 /**
282348 * Validates that input is a comma separated list of domain globs.
349+ * Can be used for fnmatch() as input.
283350 */
284351 function domain_glob_list ($ field_name , $ field_value , $ validator ) {
285352 global $ app ;
286353 $ allowempty = $ validator ['allowempty ' ] ?: 'n ' ;
287354 $ exceptions = $ validator ['exceptions ' ] ?: [];
288- if (!$ field_value ) {
289- if ($ allowempty == 'y ' ) {
355+ $ allow_exception_as_substring = $ validator ['allow_exception_as_substring ' ] ?: 'y ' ;
356+ if (!$ field_value ) {
357+ if ($ allowempty == 'y ' ) {
290358 return '' ;
291359 }
292360 return $ this ->get_error ($ validator ['errmsg ' ]);
293361 }
294362 $ parts = explode (', ' , $ field_value );
295- foreach ($ parts as $ part ) {
363+ foreach ($ parts as $ part ) {
296364 $ part = trim ($ part );
297- if (in_array ($ part , $ exceptions , true )) {
365+ // an empty part means there is a stray comma
366+ if (empty ($ part )) {
367+ return $ this ->get_error ($ validator ['errmsg ' ]);
368+ }
369+ // allow list placeholders that you will replace with real values at evaluation
370+ if (in_array ($ part , $ exceptions , true )) {
298371 continue ;
299372 }
300- if (!preg_match ("/^[a-z0-9*._-]+$/i " , $ part ) || !filter_var ($ part , FILTER_VALIDATE_DOMAIN )) {
373+ // optionally do not allow placeholders to be part of an expression
374+ if ($ allow_exception_as_substring == 'n ' ) {
375+ foreach ($ exceptions as $ exception ) {
376+ if (strpos ($ part , $ exception ) !== false ) {
377+ return $ this ->get_error ($ validator ['errmsg ' ]);
378+ }
379+ }
380+ }
381+ // A domain glob needs to:
382+ // * be a valid glob with only a-z0-9._- as characters
383+ // * have at least one dot in it
384+ // * not have two consecutive dots
385+ if (!$ this ->validate_glob ($ part , '/^[a-z0-9._-]$/ui ' ) || strpos ($ part , '. ' ) === false || strpos ($ part , '.. ' ) !== false ) {
301386 return $ this ->get_error ($ validator ['errmsg ' ]);
302387 }
303388 }
0 commit comments