Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Security Considerations: ACL, Input Validation, and CSRF Across All Areas

Security Considerations: ACL, Input Validation, and CSRF Across All Areas

~9 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026

Security was never its own block in this series - it lived inside every single controller, every API route, and every payment method as part of that chapter. This chapter pulls the scattered decisions into one coherent picture: four ACL resources, three different CSRF strategies depending on context, and one consistent rule against SQL injection across both EAV and flat collections.

The four ACL resources in the tree

The module's four ACL resources

Magento_Backend::admin
  `-- Mironsoft_Loyalty::loyalty              (chapters 6/9, top-level resource)
        |-- Mironsoft_Loyalty::rewards         (chapter 16, reward admin grid)
        `-- Mironsoft_Loyalty::points_view      (chapter 80, staff access to other customers' balances)

Magento_Config::config
  `-- Mironsoft_Loyalty::config_section        (chapters 6/7, the module's config page)

Worth noting is the deliberate split between Mironsoft_Loyalty::points_view (chapter 80) and the REST route GET /V1/loyalty/points/mine, which instead uses resource ref="self": a logged-in customer needs no explicit ACL permission to query their own balance - self is Magento's built-in mechanism for "any authenticated customer may do this for themselves". points_view applies exclusively to the second, staff-facing route GET /V1/loyalty/points/customer/:customerId, which accepts arbitrary customer IDs.

Input validation by entry point

  • EAV attributes (chapter 17): required fields like title, points_cost, and reward_type are already constrained to valid values through the attribute definition itself (required => true) and the RewardType source model (chapter 14) - validation that never even reaches the repository implementation's PHP code, because it kicks in when the attribute is saved.
  • Storefront controllers (chapter 50): Redeem\Index validates the reward ID against the active reward collection before RewardRedemptionManagementInterface::redeem() is even called - a double safeguard, since the repository implementation itself re-checks is_active (chapter 81).
  • Webapi routes (chapters 80/81): force="true" on the customerId parameter of the /mine route overwrites any client-supplied value with the ID from the auth token - the classic defense against IDOR (insecure direct object reference), without which a customer could query someone else's balance simply by changing the request parameter.
  • GraphQL resolvers (chapters 82/83): PointsSummary checks $context->getExtensionAttributes()->getIsCustomer() instead of a plain getUserId() > 0 check - exactly the distinction the GraphQL series of this catalog also drills into at a central point.

Three CSRF strategies - depending on context

Not a single POST endpoint in this module skips CSRF protection - but the concrete implementation deliberately differs by audience:

  1. Logged-in storefront customers (Redeem\Index, chapter 50): implements CsrfAwareActionInterface, both methods return null - so Magento's standard form-key validation applies unchanged, with no custom code.
  2. Guest-capable AJAX calls in checkout (Ajax\ApplyPoints, chapter 63): deliberately without AccountInterface, because a login redirect would break the AJAX call - instead a manual 401 JSON response for logged-out users instead of a redirect, while the regular form-key protection still applies.
  3. REST and GraphQL (block 10): no form-key concept needed - REST runs on token authentication, GraphQL mutations are stateless with respect to cookies; instead, ThrottleRewardRedemptionPlugin (chapter 86) handles abuse protection at the business-logic level.

Rate limiting as a security measure

RedemptionRateLimiter (chapter 86, MAX_ATTEMPTS = 5, WINDOW_SECONDS = 60) protects RewardRedemptionManagementInterface::redeem() with a single plugin against automated redemption attempts across both transports at once - REST and GraphQL call the same method, so one check is enough. The deliberately open gap: the guest-capable loyaltyRewards query has no customer context for a cache key and stays reliant on infrastructure-level protection (Nginx limit_req, chapter 86).

SQL injection: one consistent rule

addFieldToFilter() with an integer value is written without exception as ['eq' => $value] throughout this module - never as a bare scalar (CLAUDE.md, followed consistently since chapter 4). The same discipline applies to the EAV collection: addAttributeToFilter() with array operators instead of string concatenation, consistently from chapter 15 through the GraphQL data provider in chapter 82.

Achtung: None of these measures close the documented, deliberately left-open race conditions on parallel redemptions (chapters 63/81/86, the missing SELECT ... FOR UPDATE). Those are a different kind of security risk - not an injection or authorization problem, but a concurrency problem that a real production module should close before going live.

Secure building blocks alone don't make a module extensible - chapter 102 shows how other developers can build on this exact architecture without changing a single line inside Mironsoft_Loyalty itself.