Skip to content

Commit f4eac0b

Browse files
committed
Better domain glob validation #5226 #6563
allows globs like `web?.*`, `web[0-9].*`, `web[!34].*` and rejects globs like `invalid..domain` or `this is not a domain name`
1 parent 8ca4b16 commit f4eac0b

File tree

2 files changed

+91
-5
lines changed

2 files changed

+91
-5
lines changed

interface/lib/classes/validate_domain.inc.php

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

interface/web/admin/form/server_config.tform.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1653,6 +1653,7 @@
16531653
'function' => 'domain_glob_list',
16541654
'allowempty' => 'y',
16551655
'exceptions' => array('[server_name]'),
1656+
'allow_exception_as_substring' => 'n',
16561657
'errmsg'=> 'le_auto_cleanup_denylist_error_custom'
16571658
),
16581659
),

0 commit comments

Comments
 (0)