Browse Source

Perform strict validation of query parameters

This is in fact stricter than Miniflux, which ignores duplicate values
and does not validate anything other than the string enumerations
rpm
J. King 3 years ago
parent
commit
bb89083444
  1. 10
      lib/Conf.php
  2. 12
      lib/Misc/ValueInfo.php
  3. 41
      lib/REST/Miniflux/V1.php
  4. 1
      locale/en.php

10
lib/Conf.php

@ -113,14 +113,6 @@ class Conf {
/** @var \DateInterval|null (OBSOLETE) Number of seconds for SQLite to wait before returning a timeout error when trying to acquire a write lock on the database (zero does not wait) */
public $dbSQLite3Timeout = null; // previously 60.0
protected const TYPE_NAMES = [
Value::T_BOOL => "boolean",
Value::T_STRING => "string",
Value::T_FLOAT => "float",
VALUE::T_INT => "integer",
Value::T_INTERVAL => "interval",
];
protected const EXPECTED_TYPES = [
'dbTimeoutExec' => "double",
'dbTimeoutLock' => "double",
@ -318,7 +310,7 @@ class Conf {
return $value;
} catch (ExceptionType $e) {
$type = $this->types[$key]['const'] & ~(Value::M_STRICT | Value::M_DROP | Value::M_NULL | Value::M_ARRAY);
throw new Conf\Exception("typeMismatch", ['param' => $key, 'type' => self::TYPE_NAMES[$type], 'file' => $file, 'nullable' => $nullable]);
throw new Conf\Exception("typeMismatch", ['param' => $key, 'type' => Value::TYPE_NAMES[$type], 'file' => $file, 'nullable' => $nullable]);
}
}

12
lib/Misc/ValueInfo.php

@ -35,6 +35,17 @@ class ValueInfo {
public const M_DROP = 1 << 29; // drop the value (return null) if the type doesn't match
public const M_STRICT = 1 << 30; // throw an exception if the type doesn't match
public const M_ARRAY = 1 << 31; // the value should be a flat array of values of the specified type; indexed and associative are both acceptable
public const TYPE_NAMES = [
self::T_MIXED => "mixed",
self::T_NULL => "null",
self::T_BOOL => "boolean",
self::T_INT => "integer",
self::T_FLOAT => "float",
self::T_DATE => "date",
self::T_STRING => "string",
self::T_ARRAY => "array",
self::T_INTERVAL => "interval",
];
// symbolic date and time formats
protected const DATE_FORMATS = [ // in out
'iso8601' => ["!Y-m-d\TH:i:s", "Y-m-d\TH:i:s\Z" ], // NOTE: ISO 8601 dates require special input processing because of varying formats for timezone offsets
@ -48,6 +59,7 @@ class ValueInfo {
'float' => ["U.u", "U.u" ],
];
public static function normalize($value, int $type, string $dateInFormat = null, $dateOutFormat = null) {
$allowNull = ($type & self::M_NULL);
$strict = ($type & (self::M_STRICT | self::M_DROP));

41
lib/REST/Miniflux/V1.php

@ -8,6 +8,7 @@ namespace JKingWeb\Arsse\REST\Miniflux;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Feed;
use JKingWeb\Arsse\ExceptionType;
use JKingWeb\Arsse\Feed\Exception as FeedException;
use JKingWeb\Arsse\AbstractException;
use JKingWeb\Arsse\Context\Context;
@ -118,7 +119,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
'DELETE' => ["deleteCategory", false, true, false, false, []],
],
'/categories/1/entries' => [
'GET' => ["getCategoryEntries", false, true, false, false, []],
'GET' => ["getCategoryEntries", false, true, false, true, []],
],
'/categories/1/entries/1' => [
'GET' => ["getCategoryEntry", false, true, false, false, []],
@ -155,7 +156,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
'DELETE' => ["deleteFeed", false, true, false, false, []],
],
'/feeds/1/entries' => [
'GET' => ["getFeedEntries", false, true, false, false, []],
'GET' => ["getFeedEntries", false, true, false, true, []],
],
'/feeds/1/entries/1' => [
'GET' => ["getFeedEntry", false, true, false, false, []],
@ -226,7 +227,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
return new ErrorResponse("401", 401);
}
// get the request path only; this is assumed to already be normalized
$target = parse_url($req->getRequestTarget())['path'] ?? "";
$target = parse_url($req->getRequestTarget(), \PHP_URL_PATH) ?? "";
$method = $req->getMethod();
// handle HTTP OPTIONS requests
if ($method === "OPTIONS") {
@ -270,7 +271,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
$args[] = $data;
}
if ($reqQuery) {
$args[] = $req->getQueryParams();
$args[] = $this->normalizeQuery(parse_url($req->getRequestTarget(), \PHP_URL_QUERY) ?? "");
}
try {
return $this->$func(...$args);
@ -330,9 +331,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
} elseif (gettype($body[$k]) !== $t) {
return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])], 422);
} elseif (
(in_array($k, ["keeplist_rules", "blocklist_rules"]) && !Rule::validate($body[$k])) ||
(in_array($k, ["url", "feed_url"]) && !URL::absolute($body[$k])) ||
($k === "category_id" && $body[$k] < 1)
(in_array($k, ["keeplist_rules", "blocklist_rules"]) && !Rule::validate($body[$k]))
|| (in_array($k, ["url", "feed_url"]) && !URL::absolute($body[$k]))
|| ($k === "category_id" && $body[$k] < 1)
) {
return new ErrorResponse(["InvalidInputValue", 'field' => $k], 422);
}
@ -359,7 +360,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
return $body;
}
protected function normalizeQuery(string $query): array {
protected function normalizeQuery(string $query) {
// fill an array with all valid keys
$out = [];
foreach (self::VALID_QUERY as $k => $t) {
@ -376,10 +377,26 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
}
$t = self::VALID_QUERY[$k] & ~V::M_ARRAY;
$a = self::VALID_QUERY[$k] >= V::M_ARRAY;
if ($a) {
$out[$k][] = V::normalize($v, $t + V::M_DROP, "unix");
} elseif (!isset($out[$k])) {
$out[$k] = V::normalize($v, $t + V::M_DROP, "unix");
try {
if ($a) {
$out[$k][] = V::normalize($v, $t + V::M_STRICT, "unix");
} elseif (!isset($out[$k])) {
$out[$k] = V::normalize($v, $t + V::M_STRICT, "unix");
} else {
return new ErrorResponse(["DuplicateInputValue", 'field' => $k], 400);
}
} catch (ExceptionType $e) {
return new ErrorResponse(["InvalidInputValue", 'field' => $k], 400);
}
if (
// TODO: does the "starred" param accept 0/1, or just true/false?
(in_array($k, ["category_id", "before_entry_id", "after_entry_id"]) && $v < 1)
|| (in_array($k, ["limit", "offset"]) && $v < 0)
|| ($k === "direction" && !in_array($v, ["asc", "desc"]))
|| ($k === "order" && !in_array($v, ["id", "status", "published_at", "category_title", "category_id"]))
|| ($k === "status" && !in_array($v, ["read", "unread", "removed"]))
) {
return new ErrorResponse(["InvalidInputValue", 'field' => $k], 400);
}
}
return $out;

1
locale/en.php

@ -12,6 +12,7 @@ return [
'API.Miniflux.Error.403' => 'Access Forbidden',
'API.Miniflux.Error.404' => 'Resource Not Found',
'API.Miniflux.Error.MissingInputValue' => 'Required key "{field}" was not present in input',
'API.Miniflux.Error.DuplicateInputValue' => 'Key "{field}" accepts only one value',
'API.Miniflux.Error.InvalidBodyJSON' => 'Invalid JSON payload: {0}',
'API.Miniflux.Error.InvalidInputType' => 'Input key "{field}" of type {actual} was expected as {expected}',
'API.Miniflux.Error.InvalidInputValue' => 'Supplied value is not valid for input key "{field}"',

Loading…
Cancel
Save