Skip to content

Make EntityMerger an allowlist; fix Ignore & falsy-merge bugs (#523)#553

Open
maltehuebner wants to merge 1 commit into
mainfrom
fix/entity-merger-mass-assignment-523
Open

Make EntityMerger an allowlist; fix Ignore & falsy-merge bugs (#523)#553
maltehuebner wants to merge 1 commit into
mainfrom
fix/entity-merger-mass-assignment-523

Conversation

@maltehuebner

Copy link
Copy Markdown
Contributor

Summary

EntityMerger (used by StationApiController::postStationAction and CityApiController::postCityAction) copied every reflected property with no effective guard, enabling mass assignment onto identity/key fields. Three defects fixed.

Changes

1. Mass assignment -> explicit allowlist

Merging is now opt-in via a dedicated #[App\Air\Util\EntityMerger\Attribute\Mergeable] attribute (separate from the serializer's #[Ignore], which is a serialization concern). Only annotated properties are merged.

  • Mergeable: Station title, latitude, longitude, ubaStationId, fromDate, untilDate, altitude; City name, description.
  • Protected by default (not annotated): Station::$stationCode, Station::$provider, City::$slug (identity keys), plus id, createdAt, relations and the derived coord. These can no longer be changed through the API.

2. Wirkungsloser #[Ignore]-Check (always-false instanceof)

isPropertyExposed() did $reflectionAttribute instanceof Ignore, but getAttributes() returns ReflectionAttribute objects, never Ignore instances — so it always returned true. Replaced with the explicit #[Mergeable] allowlist check.

3. Falsy values could not be set

if ($newValue) discarded 0, 0.0, '', false (e.g. altitude = 0 at sea level, a coordinate at exactly 0.0). Changed to null !== $newValue; only null means "not provided". Uninitialized typed properties on serializer-hydrated sources are skipped via ReflectionProperty::isInitialized() (the old catch (\TypeError) didn't even catch the Error that uninitialized-property access throws).

Housekeeping

  • Removed the two now-obsolete phpstan-baseline.neon entries (always-false instanceof; mismatched @var).
  • Added tests/Air/Util/EntityMerger/EntityMergerTest.php (6 cases: mergeable copied, identity protected, falsy 0 merged, null skipped, uninitialized typed property skipped, returns destination).
  • Updated the CLAUDE.md EntityMerger security note.

Verification

  • vendor/bin/phpstan analyse — No errors.
  • vendor/bin/phpunit --testsuite="Project Test Suite" — 127 tests green (incl. the 6 new).
  • bin/console lint:container --env=prod — OK.

Note: end-to-end exercise of the two POST endpoints needs a populated PostGIS DB (not available here); the merge logic is covered by the unit test above.

Closes #523

🤖 Generated with Claude Code

EntityMerger copied every property without a working guard, letting the
update endpoints (StationApiController::postStationAction,
CityApiController::postCityAction) overwrite identity/key fields via
mass assignment. Three problems fixed:

1) Mass assignment (allowlist instead of denylist): merging is now opt-in
   via a dedicated #[App\Air\Util\EntityMerger\Attribute\Mergeable]
   attribute. Only annotated properties are merged. Identity keys
   (Station::$stationCode, Station::$provider, City::$slug) and system
   fields (id, createdAt, relations, derived coord) are excluded by
   default and can no longer be changed through the API. Annotated as
   mergeable: Station title/latitude/longitude/ubaStationId/fromDate/
   untilDate/altitude and City name/description.

2) Wirkungsloser #[Ignore]-Check: the old isPropertyExposed() compared
   ReflectionAttribute instances with `instanceof Ignore`, which is always
   false. Replaced by the explicit Mergeable allowlist check.

3) Falsy values discarded: the old `if ($newValue)` guard dropped 0, 0.0,
   '' and false (e.g. altitude = 0 at sea level, coordinates at exactly 0).
   Changed to `null !== $newValue`; only null now means "not provided".
   Uninitialized typed properties on serializer-hydrated sources are
   skipped via ReflectionProperty::isInitialized() instead of catching an
   error.

Removed the two now-obsolete phpstan-baseline entries (always-false
instanceof, mismatched @var), added a unit test for EntityMerger, and
updated the CLAUDE.md security note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Hoch] Mass Assignment im EntityMerger + wirkungsloser #[Ignore]-Check

1 participant