Skip to content

Commit

Permalink
Add where parameter and deprecate filters parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
guvra committed Mar 5, 2024
1 parent c8904be commit 6147375
Show file tree
Hide file tree
Showing 13 changed files with 332 additions and 60 deletions.
3 changes: 3 additions & 0 deletions app/config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@
"skip_conversion_if": {
"type": ["string", "number"]
},
"where": {
"type": ["string"]
},
"filters": {
"type": "array",
"items": {
Expand Down
11 changes: 2 additions & 9 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,8 @@
<property name="linesCountBetweenDescriptionAndAnnotations" value="1"/>
<property name="linesCountBetweenAnnotationsGroups" value="1"/>
<property name="annotationsGroups" type="array">
<element value="
@internal,
@deprecated,
"/>
<element value="
@param\,
@return\,
@throws\,
"/>
<element value="@inheritdoc"/>
<element value="@internal,@deprecated,@var,@param,@return,@throws"/>
</property>
</properties>
</rule>
Expand Down
1 change: 1 addition & 0 deletions src/Converter/Anonymizer/AnonymizeDate.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public function setParameters(array $parameters): void

/**
* @inheritdoc
*
* @throws UnexpectedValueException
*/
public function convert(mixed $value, array $context = []): string
Expand Down
15 changes: 12 additions & 3 deletions src/Dumper/Config/DumperConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

class DumperConfig
{
private QueryValidator $selectQueryValidator;
private QueryValidator $initCommandQueryValidator;

/**
* @var TableConfig[]
*/
Expand Down Expand Up @@ -90,6 +93,8 @@ class DumperConfig
*/
public function __construct(ConfigInterface $config)
{
$this->selectQueryValidator = new QueryValidator(['select']);
$this->initCommandQueryValidator = new QueryValidator(['set']);
$this->prepareConfig($config);
}

Expand Down Expand Up @@ -260,6 +265,11 @@ private function prepareDumpSettings(ConfigInterface $config): void
$this->dumpSettings[$param] = $value;
}

// Validate init_commands
foreach ($this->dumpSettings['init_commands'] as $query) {
$this->initCommandQueryValidator->validate($query);
}

// Replace {...} by the current date in dump output
$this->dumpSettings['output'] = preg_replace_callback(
'/{([^}]+)}/',
Expand Down Expand Up @@ -327,7 +337,7 @@ private function prepareTablesConfig(ConfigInterface $config): void
$this->tablesToSort[] = $tableConfig->getName();
}

if ($tableConfig->hasFilter() || $tableConfig->hasLimit()) {
if ($tableConfig->hasWhereCondition() || $tableConfig->hasFilter() || $tableConfig->hasLimit()) {
$this->tablesToFilter[] = $tableConfig->getName();
}
}
Expand All @@ -338,11 +348,10 @@ private function prepareTablesConfig(ConfigInterface $config): void
*/
private function prepareVarQueries(ConfigInterface $config): void
{
$queryValidator = new QueryValidator();
$this->varQueries = $config->get('variables', []);

foreach ($this->varQueries as $index => $query) {
$queryValidator->validate($query);
$this->selectQueryValidator->validate($query);
$this->varQueries[$index] = (string) $query;
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/Dumper/Config/Table/Filter/Filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

use UnexpectedValueException;

/**
* @deprecated
*/
class Filter
{
public const OPERATOR_EQ = 'eq';
Expand Down
32 changes: 32 additions & 0 deletions src/Dumper/Config/Table/TableConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@

use Smile\GdprDump\Dumper\Config\Table\Filter\Filter;
use Smile\GdprDump\Dumper\Config\Table\Filter\SortOrder;
use Smile\GdprDump\Dumper\Config\Validation\WhereExprValidator;
use UnexpectedValueException;

class TableConfig
{
private WhereExprValidator $whereExprValidator;
private string $name;
private ?string $where = null;
private ?int $limit = null;
private array $converters = [];
private string $skipCondition = '';

/**
* @deprecated
* @var Filter[]
*/
private array $filters = [];
Expand All @@ -27,6 +31,7 @@ class TableConfig

public function __construct(string $tableName, array $tableConfig)
{
$this->whereExprValidator = new WhereExprValidator();
$this->name = $tableName;
$this->prepareConfig($tableConfig);
}
Expand All @@ -42,13 +47,22 @@ public function getName(): string
/**
* Get the filters.
*
* @deprecated
* @return Filter[]
*/
public function getFilters(): array
{
return $this->filters;
}

/**
* Get the where condition.
*/
public function getWhereCondition(): ?string
{
return $this->where;
}

/**
* Get the sort orders.
*
Expand Down Expand Up @@ -77,12 +91,22 @@ public function getConverters(): array

/**
* Check if there is data to filter.
*
* @deprecated
*/
public function hasFilter(): bool
{
return !empty($this->filters);
}

/**
* Check if there is data to filter (with `where` param).
*/
public function hasWhereCondition(): bool
{
return $this->where !== null;
}

/**
* Check if the table data must be sorted.
*/
Expand Down Expand Up @@ -124,11 +148,19 @@ private function prepareConfig(array $tableData): void
*/
private function prepareFilters(array $tableData): void
{
// Old way of declaring table filters (`filters` parameter)
if (isset($tableData['filters'])) {
foreach ($tableData['filters'] as $filter) {
$this->filters[] = new Filter((string) $filter[0], (string) $filter[1], $filter[2] ?? null);
}
}

// New way of declaring table filters (`where` parameter)
if (isset($tableData['where'])) {
$whereCondition = (string) $tableData['where'];
$this->whereExprValidator->validate($whereCondition);
$this->where = $whereCondition;
}
}

/**
Expand Down
73 changes: 56 additions & 17 deletions src/Dumper/Config/Validation/QueryValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Smile\GdprDump\Dumper\Config\Validation;

use TheSeer\Tokenizer\TokenCollection;
use TheSeer\Tokenizer\Tokenizer;

class QueryValidator
Expand All @@ -13,37 +14,75 @@ class QueryValidator
/**
* @var string[]
*/
private array $statementBlacklist = [
'grant', 'revoke', 'create', 'alter', 'drop', 'rename',
'insert', 'update', 'delete', 'truncate', 'replace',
'prepare', 'execute', 'lock', 'unlock', 'optimize', 'repair',
private array $statements = [
'alter', 'analyse', 'backup', 'binlog', 'cache', 'change', 'close', 'commit', 'create',
'deallocate', 'declare', 'delete', 'describe', 'drop', 'execute', 'explain', 'fetch', 'flush',
'get', 'grant', 'help', 'install', 'kill', 'load', 'lock', 'open', 'optimize', 'prepare',
'purge', 'rename', 'repair', 'reset', 'resignal', 'revoke', 'rollback', 'savepoint', 'select',
'set', 'password', 'show', 'shutdown', 'signal', 'start', 'truncate', 'uninstall', 'unlock',
'update', 'use', 'xa',
];

/**
* Create the query validator.
* @var string[]
*/
private array $allowedStatements;

/**
* @param string[] $allowedStatements
*/
public function __construct()
public function __construct(array $allowedStatements)
{
$this->tokenizer = new Tokenizer();

// Better performance to check array keys
$this->statements = array_flip($this->statements);
$this->allowedStatements = array_flip($allowedStatements);
}

/**
* Validate that a SQL query is safe for execution.
*
* @throws ValidationException
* Validate the query. An optional callback can be passed for additional validation.
*/
public function validate(string $query): void
public function validate(string $query, ?callable $callback = null): void
{
// Use a PHP tokenizer to split the query into tokens
$tokens = $this->tokenizer->parse('<?php ' . strtolower($query) . '?>');
$tokens = $this->tokenize($query);

foreach ($tokens as $token) {
// If the token is a word, check if it contains a forbidden statement
if ($token->getName() === 'T_STRING' && in_array($token->getValue(), $this->statementBlacklist, true)) {
$message = 'The following query contains a forbidden keyword: "%s". '
. 'You may use "`%s`" to prevent this error.';
throw new ValidationException(sprintf($message, $query, $token->getValue()));
$name = $token->getName();
$value = $token->getValue();

if ($name === 'T_DEC' || $name === 'T_COMMENT') {
throw new ValidationException(sprintf('Forbidden comment found in query "%s".', $query));
}

if ($name === 'T_STRING' && !$this->isStatementAllowed($value)) {
throw new ValidationException(sprintf('Forbidden keyword "%s" found in query "%s".', $value, $query));
}

if ($callback !== null) {
$callback($token);
}
}
}

/**
* Tokenize the query.
*/
private function tokenize(string $query): TokenCollection
{
return $this->tokenizer->parse('<?php ' . strtolower($query) . '?>');
}

/**
* Check whether the statement is allowed.
*/
private function isStatementAllowed(string $statement): bool
{
if (empty($this->allowedStatements)) {
return !array_key_exists($statement, $this->statements);
}

return array_key_exists($statement, $this->allowedStatements)
|| !array_key_exists($statement, $this->statements);
}
}
41 changes: 41 additions & 0 deletions src/Dumper/Config/Validation/WhereExprValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Smile\GdprDump\Dumper\Config\Validation;

use TheSeer\Tokenizer\Token;

class WhereExprValidator
{
private QueryValidator $queryValidator;

public function __construct()
{
$this->queryValidator = new QueryValidator(['select']);
}

/**
* Validate the where expression.
*/
public function validate(string $expr): void
{
$openedBrackets = 0;

$this->queryValidator->validate($expr, function (Token $token) use ($expr, &$openedBrackets): void {
// Disallow using a closing bracket if there is no matching opening bracket -> prevents SQL injection
if ($token->getName() === 'T_OPEN_BRACKET') {
++$openedBrackets;
return;
}

if ($token->getName() === 'T_CLOSE_BRACKET') {
if ($openedBrackets === 0) {
throw new ValidationException(sprintf('Unmatched closing bracket found in query "%s".', $expr));
}

--$openedBrackets;
}
});
}
}
Loading

0 comments on commit 6147375

Please sign in to comment.