|
@@ -29,6 +29,15 @@ final class AuditController
|
|
|
// entity types/ids short identifiers); 128 is comfortable headroom.
|
|
// entity types/ids short identifiers); 128 is comfortable headroom.
|
|
|
private const MAX_FILTER_LENGTH = 128;
|
|
private const MAX_FILTER_LENGTH = 128;
|
|
|
|
|
|
|
|
|
|
+ // SEC_REVIEW F47: charset gate for the *_kind columns the audit
|
|
|
|
|
+ // filters touch (`subject_kind`, `entity_type`). Every known kind
|
|
|
|
|
+ // is a short snake/kebab identifier (`reporter`, `consumer`,
|
|
|
|
|
+ // `admin-token`, `manual_block`, `oidc_role_mapping`); reject
|
|
|
|
|
+ // anything that isn't a-z0-9 plus `_-` so a caller can't push
|
|
|
|
|
+ // bytes that wouldn't match any real `target_type` / `actor_kind`
|
|
|
|
|
+ // column value through the prepared statement.
|
|
|
|
|
+ private const KIND_PATTERN = '/^[a-z0-9][a-z0-9_-]*$/';
|
|
|
|
|
+
|
|
|
// SEC_REVIEW F31: cap pagination depth so a Viewer cannot force
|
|
// SEC_REVIEW F31: cap pagination depth so a Viewer cannot force
|
|
|
// `LIMIT n OFFSET huge` deep scans. 10 000 rows is well past any
|
|
// `LIMIT n OFFSET huge` deep scans. 10 000 rows is well past any
|
|
|
// human-driven browse and bounds the worst-case scan cost.
|
|
// human-driven browse and bounds the worst-case scan cost.
|
|
@@ -86,6 +95,8 @@ final class AuditController
|
|
|
if (isset($params['entity_type']) && is_string($params['entity_type']) && $params['entity_type'] !== '') {
|
|
if (isset($params['entity_type']) && is_string($params['entity_type']) && $params['entity_type'] !== '') {
|
|
|
if (strlen($params['entity_type']) > self::MAX_FILTER_LENGTH) {
|
|
if (strlen($params['entity_type']) > self::MAX_FILTER_LENGTH) {
|
|
|
$errors['entity_type'] = 'must be at most ' . self::MAX_FILTER_LENGTH . ' characters';
|
|
$errors['entity_type'] = 'must be at most ' . self::MAX_FILTER_LENGTH . ' characters';
|
|
|
|
|
+ } elseif (preg_match(self::KIND_PATTERN, $params['entity_type']) !== 1) {
|
|
|
|
|
+ $errors['entity_type'] = 'must match `[a-z0-9][a-z0-9_-]*`';
|
|
|
} else {
|
|
} else {
|
|
|
$filters['entity_type'] = $params['entity_type'];
|
|
$filters['entity_type'] = $params['entity_type'];
|
|
|
}
|
|
}
|
|
@@ -109,6 +120,11 @@ final class AuditController
|
|
|
$errors['subject'] = 'subject_kind and subject_id must be supplied together';
|
|
$errors['subject'] = 'subject_kind and subject_id must be supplied together';
|
|
|
} elseif (strlen($subjectKind) > self::MAX_FILTER_LENGTH || strlen($subjectId) > self::MAX_FILTER_LENGTH) {
|
|
} elseif (strlen($subjectKind) > self::MAX_FILTER_LENGTH || strlen($subjectId) > self::MAX_FILTER_LENGTH) {
|
|
|
$errors['subject'] = 'subject_kind and subject_id must each be at most ' . self::MAX_FILTER_LENGTH . ' characters';
|
|
$errors['subject'] = 'subject_kind and subject_id must each be at most ' . self::MAX_FILTER_LENGTH . ' characters';
|
|
|
|
|
+ } elseif (preg_match(self::KIND_PATTERN, $subjectKind) !== 1) {
|
|
|
|
|
+ // SEC_REVIEW F47: subject_kind is matched against
|
|
|
|
|
+ // `target_type` / `actor_kind` columns whose real values
|
|
|
|
|
+ // are all short snake/kebab identifiers.
|
|
|
|
|
+ $errors['subject'] = 'subject_kind must match `[a-z0-9][a-z0-9_-]*`';
|
|
|
} else {
|
|
} else {
|
|
|
$filters['subject_kind'] = $subjectKind;
|
|
$filters['subject_kind'] = $subjectKind;
|
|
|
$filters['subject_id'] = $subjectId;
|
|
$filters['subject_id'] = $subjectId;
|