Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions core/src/Core.php
Original file line number Diff line number Diff line change
Expand Up @@ -5398,7 +5398,7 @@ public function getTemplateVars($idnames = [], $fields = '*', $docid = '', $publ

// get document record
if (empty($docid)) {
$docid = $this->documentIdentifier;
$docid = (int)$this->documentIdentifier;
$docRow = $this->documentObject;
} else {
$docRow = $this->getDocument($docid, '*', $published, 0, $checkAccess);
Expand All @@ -5407,6 +5407,8 @@ public function getTemplateVars($idnames = [], $fields = '*', $docid = '', $publ
$cached[$cacheKey] = false;
return false;
}
// The id reaches raw SQL below: use the one the database returned, never the caller's value
$docid = (int)($docRow['id'] ?? 0);
}
$table = $this->getDatabase()->getFullTableName('site_tmplvars');
// get user defined template variables
Expand Down Expand Up @@ -5442,7 +5444,7 @@ public function getTemplateVars($idnames = [], $fields = '*', $docid = '', $publ
$join->on('site_tmplvar_contentvalues.tmplvarid', '=', 'site_tmplvars.id');
$join->on('site_tmplvar_contentvalues.contentid', '=', \DB::raw($docid));
})
->whereRaw($query . " AND " . $this->getDatabase()->getConfig('prefix') . "site_tmplvar_templates.templateid = '" . $docRow['template'] . "'");
->whereRaw($query . " AND " . $this->getDatabase()->getConfig('prefix') . "site_tmplvar_templates.templateid = '" . (int)$docRow['template'] . "'");
if ($sort != '') {
$rs = $rs->orderByRaw($sort);
}
Expand Down
11 changes: 10 additions & 1 deletion core/src/Models/SiteContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ class SiteContent extends Eloquent\Model
*/
const MAX_TV_QUERY_TERMS = 20;

/**
* Operators a tvFilter() term may use. The operator is spliced into raw SQL for numeric casts,
* so it has to be an exact match, not a pattern.
*/
const TV_FILTER_OPERATORS = [
'=', '!=', '<>', '<', '>', '<=', '>=',
'like', 'like-l', 'like-r', 'in', 'not_in', 'isnull', 'null', 'isnotnull', '!null',
];

/**
* ClosureTable model instance.
*
Expand Down Expand Up @@ -2244,7 +2253,7 @@ public function scopeTvFilter($query, $filters = '', $outerSep = ';', $innerSep
$cast = !empty($parts[4]) ? $parts[4] : '';
// The name, operator and cast end up in raw SQL below, so refuse anything that is not plain
if (!preg_match('/^[\w\-]+$/D', (string)$tvname)
|| !preg_match('/^(=|!=|<>|<=|>=|<|>|[a-z_\-!]+)$/iD', (string)$op)
|| !in_array(strtolower((string)$op), self::TV_FILTER_OPERATORS, true)
|| !preg_match('/^([A-Za-z]+(\(\d+(,\d+)?\))?)?$/D', (string)$cast)) {
// Fail closed: dropping a malformed filter would widen the result set
$query = $query->whereRaw('1 = 0');
Expand Down
23 changes: 23 additions & 0 deletions core/tests/Unit/Security/TreeSortAllowlistTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ public function whereRaw($sql)
return $this;
}

public function __call($method, $args)
{
$this->raw[] = $method . ':' . json_encode($args);

return $this;
}

public function orderBy(...$args)
{
$this->raw[] = 'orderBy:' . json_encode($args);
Expand Down Expand Up @@ -104,3 +111,19 @@ public function where(...$args)
expect($q->raw)->toHaveCount($max);
$GLOBALS['evo'] = null;
});

test('a tv filter operator outside the allowlist matches nothing', function (string $op) {
$q = recordingTvQuery();
(new \EvolutionCMS\Models\SiteContent())->scopeTvFilter($q, "tv:price:{$op}:1:UNSIGNED");

expect($q->raw)->toBe(['1 = 0']);
$GLOBALS['evo'] = null;
})->with(['OR', 'AND', 'xor', 'is-not', 'div', 'regexp-x']);

test('every allowlisted tv filter operator is still applied', function (string $op) {
$q = recordingTvQuery();
(new \EvolutionCMS\Models\SiteContent())->scopeTvFilter($q, "tv:price:{$op}:1:UNSIGNED");

expect($q->raw)->not->toBe(['1 = 0']);
$GLOBALS['evo'] = null;
})->with(\EvolutionCMS\Models\SiteContent::TV_FILTER_OPERATORS);
Loading