Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

fix(security): wave-7 critical C1 (RevertHandler RBAC) + C2 (@self injection allowlist)#2008

Merged
rubenvdlinde merged 1 commit into
developmentfrom
fix/wave7-critical-c1-c2-security
May 28, 2026
Merged

fix(security): wave-7 critical C1 (RevertHandler RBAC) + C2 (@self injection allowlist)#2008
rubenvdlinde merged 1 commit into
developmentfrom
fix/wave7-critical-c1-c2-security

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Summary

This PR closes two wave-7 CRITICAL security findings from the deep team-reviewer pass (2026-05-27). See project_deep-review-findings-2026-05-27.md in project memory.

CRITICAL #1 — RevertHandler bypasses RBAC + multitenancy

File: lib/Service/Object/RevertHandler.php

Vulnerability: revert() called findAcrossAllSources(_rbac: false, _multitenancy: false), allowing any authenticated user to revert any object in any tenant. NotAuthorizedException was imported but never thrown.

Fix:

  • Added the missing constructor with PermissionHandler dependency
  • Removed _rbac: false / _multitenancy: false — the find is now tenant-scoped with RBAC enforced
  • After the find, permissionHandler->hasPermission(schema, 'update', object) is checked; throws NotAuthorizedException on denial (correctly caught by RevertController for a 403)

CRITICAL #2 — Client-supplied @self overwrites server-controlled security fields

Files: lib/Service/Object/SaveObject.php, lib/Db/MagicMapper.php

Vulnerability: A client POST with {"@self": {"owner": "admin"}} set the object owner to an arbitrary value. For authenticated REST requests applyOwnerAttribution() overrode this — but for background/system contexts (OpenConnector sync, cron) with no IUserSession user, applyOwnerAttribution only fills owner when empty, so the client-controlled value persisted.

Fix (two layers):

Layer 1 — SaveObject::setSelfMetadata (primary): Removed the owner setter from the method. applyOwnerAttribution() is now the sole authoritative owner setter.

Layer 2 — MagicMapper::prepareObjectDataForTable (defense-in-depth): Strips owner and authorization from the @self block before DB write. Makes register and schema override unconditional (was conditional-on-empty), preventing identity field injection.

Tests

Four test files updated to invert owner-injection assertions (owner-is-set → owner-is-null/ignored):

  • tests/Unit/Service/Object/SaveObjectDeepTest.php
  • tests/Unit/Service/Object/SaveObjectTest.php
  • tests/Unit/Service/ObjectHandlers/SaveObjectCoverageTest.php
  • tests/Unit/Service/MagicMapperTest.php

Gate output:

  • php -l: clean on all 7 changed files
  • phpstan: no new errors (2 pre-existing test-file errors unchanged)
  • phpunit: 457/457 pass on all modified test files

Do not admin-merge — production repo, requires user review.

…jection allowlist)

CRITICAL #1 — RevertHandler RBAC bypass (C1):
RevertHandler::revert() called findAcrossAllSources with _rbac:false and
_multitenancy:false, letting any authenticated user revert any object in
any tenant without permission checks. NotAuthorizedException was imported
but never thrown.

Fix:
- Add missing constructor with AuditTrailMapper, ContainerInterface,
  IEventDispatcher, MagicMapper, and new PermissionHandler dependency.
- Remove _rbac:false / _multitenancy:false flags — use tenant-scoped find.
- After the find, call permissionHandler->hasPermission(schema, 'update',
  object) and throw NotAuthorizedException when denied.

CRITICAL #2 — Client-supplied @self overwrites server-controlled fields (C2):

Two-layer fix:

Layer 1 — SaveObject::setSelfMetadata (primary):
The method accepted owner from raw client $selfData and applied it to the
entity before applyOwnerAttribution() ran. For logged-in REST requests,
applyOwnerAttribution() overrode it; for background/system contexts with
no IUserSession user, the client-supplied owner persisted. Removed the
owner setter from setSelfMetadata — applyOwnerAttribution() is the sole
authoritative owner setter.

Layer 2 — MagicMapper::prepareObjectDataForTable (defense-in-depth):
Client could inject @self.owner and @self.authorization via the @self
block. Added explicit strip of owner and authorization from $metadata
before the DB write. Also made the register/schema override unconditional
(previously only applied when empty) to prevent identity field injection.

Tests updated: SaveObjectDeepTest, SaveObjectTest, SaveObjectCoverageTest
(owner-setting assertions inverted to assert null), MagicMapperTest
(register/schema values changed to verify server-param override; owner
column asserted null).

gate output: php -l clean on all 7 files; phpstan no new errors (2
pre-existing errors in test files unchanged); phpunit 457/457 pass on
modified test files.

References wave-7 deep review (project_deep-review-findings-2026-05-27.md)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant