security: Replace params-driven constantize with allowlists (WA-RCE-001)#1025
security: Replace params-driven constantize with allowlists (WA-RCE-001)#1025kitcommerce merged 2 commits intonextfrom
Conversation
Fixes #802 Brakeman flagged three High-confidence UnsafeReflection / Remote Code Execution warnings where user-controlled request parameters were passed directly to .constantize. Changes ------- ### admin/app/controllers/workarea/admin/bulk_actions_controller.rb - Remove `params[:type].constantize`. - Add `bulk_action_class_for` helper that iterates over the new `Workarea.config.bulk_action_types` allowlist (config strings are constantized, never the raw parameter value) and matches by class name. - Returns `nil` (→ 422) for anything not in the allowlist. - Retains the existing `raise unless klass < BulkAction` guard as defense-in-depth. ### admin/app/controllers/workarea/admin/create_segments_controller.rb ### admin/app/controllers/workarea/admin/segment_rules_controller.rb - Remove direct `"Workarea::Segment::Rules::\#{params[:rule_type]}...".constantize`. - Add `segment_rule_class_for` helper that matches the slug against classes drawn from the existing `Workarea.config.segment_rule_types` config list; constantize is called on the allowlisted strings only. - Returns `nil` (→ 422) for unknown slugs. ### core/app/queries/workarea/admin_search_query_wrapper.rb - Guard `klass` with an allowlist check against the new `Workarea.config.admin_exportable_models` config list. - Raises `ArgumentError` with an actionable message for unknown types, so operators and developers get clear feedback when a plugin model needs to be added to the allowlist. ### core/lib/workarea/configuration.rb - Add `config.bulk_action_types` (SwappableList, 3 built-in types). - Add `config.admin_exportable_models` (SwappableList, 21 built-in models reflecting all usages found in admin views). Both lists are append-safe for plugin authors. Tests ----- - `BulkActionsIntegrationTest`: two new tests — rejects unknown type, rejects non-BulkAction subclass. - `SegmentRulesIntegrationTest`: rejects unknown rule_type slug. - `CreateSegmentsIntegrationTest`: rejects unknown rule_type slug in new_rule action. - `AdminSearchQueryWrapperTest`: rejects model_type not in allowlist, rejects blank model_type.
Simplicity ReviewVerdict: PASS_WITH_NOTES The security fix is correct and the allowlist pattern is sound. Two simplification opportunities worth addressing. Finding 1 —
|
🏗️ Architecture ReviewVerdict: PASS_WITH_NOTES The allowlist pattern is architecturally sound and fits Workarea's extension model well. SwappableList config entries in Findings1. Duplicate
2. Inconsistent error semantics between controllers and query wrapper (LOW)
3.
4. Controller helper placement is appropriate (OK) 5. 422 response codes are semantically correct (OK) SummaryClean, minimal, plugin-safe. The SwappableList + config pattern is exactly how Workarea expects extensibility to work. The two notes above are minor polish items that could be addressed in a follow-up. Nothing here should block merge. {
"reviewer": "architecture",
"verdict": "PASS_WITH_NOTES",
"severity": "LOW",
"summary": "Allowlist pattern is architecturally sound and plugin-safe via SwappableList. Minor DRY violation in segment_rule_class_for duplication and an error-handling inconsistency in AdminSearchQueryWrapper worth addressing in follow-up.",
"findings": [
{
"issue": "segment_rule_class_for is duplicated in CreateSegmentsController and SegmentRulesController",
"severity": "LOW",
"suggestion": "Extract to a shared concern (e.g., Workarea::Admin::SegmentRuleLookup)"
},
{
"issue": "AdminSearchQueryWrapper raises ArgumentError (→ 500) while controllers return 422 for the same class of error",
"severity": "LOW",
"suggestion": "Verify upstream rescue exists or align error handling strategy"
},
{
"issue": "Existing segment_rule_types uses plain array while new configs use SwappableList",
"severity": "INFO",
"suggestion": "Consider upgrading segment_rule_types to SwappableList in a follow-up"
}
]
} |
🔒 Rails Security ReviewReviewer: rails-security SummaryThe core security fix is solid and well-implemented. All three Findings1. Unhandled
|
| Check | Status |
|---|---|
Endpoints behind require_login + require_admin |
✅ Confirmed via Admin::ApplicationController |
check_authorization before_action present |
✅ Applies to all admin actions except dashboard |
No constantize on raw params |
✅ All three sites use allowlist lookup |
| BulkAction subclass check preserved | ✅ raise unless klass < BulkAction still present |
| 422 responses leak no sensitive data | ✅ head :unprocessable_entity returns empty body |
| SwappableList mutability | ✅ Intentionally mutable for plugin extensibility — standard Workarea pattern |
| Test coverage for rejection paths | ✅ Tests for unknown types, non-subclass types, and blank model_type |
Segment rule lookup uses existing segment_rule_types config |
✅ No new config needed |
Verdict JSON
{
"reviewer": "rails-security",
"verdict": "PASS_WITH_NOTES",
"severity": "LOW",
"summary": "Core RCE fix is sound — constantize never touches raw params. Two low-severity notes: ArgumentError in AdminSearchQueryWrapper could 500 instead of 422, and pre-existing params.permit! is noted for awareness.",
"findings": [
{
"issue": "ArgumentError in AdminSearchQueryWrapper#klass bubbles as 500 instead of 422",
"severity": "LOW",
"suggestion": "Rescue ArgumentError at the controller layer or return nil from #klass for caller-driven error handling"
},
{
"issue": "params.permit! in Admin::ApplicationController permits all params (pre-existing)",
"severity": "INFORMATIONAL",
"suggestion": "Separate concern — not introduced by this PR, but worth tracking"
}
]
}
🔒 Security ReviewVerdict: PASS_WITH_NOTES SummaryThe allowlist pattern correctly eliminates all 3 Brakeman RCE (UnsafeReflection) findings. Analysis
Findings1. ArgumentError echoes user input (LOW)
2. Config entry NameError propagation (INFORMATIONAL)
Verdict Detail{
"reviewer": "security",
"verdict": "PASS_WITH_NOTES",
"severity": "LOW",
"summary": "Allowlist pattern correctly eliminates all 3 RCE findings. No bypass vectors. Two minor notes: ArgumentError echoes user input (no external exposure in production), and invalid config entries would raise at request time.",
"findings": [
{
"issue": "ArgumentError in AdminSearchQueryWrapper#klass interpolates user-controlled model_type into exception message",
"severity": "LOW",
"suggestion": "Use static error message without interpolating user input, or log raw value separately"
},
{
"issue": "Invalid config entries in bulk_action_types would raise NameError at request time, not boot time",
"severity": "INFORMATIONAL",
"suggestion": "No action required — consistent with existing patterns and caught by tests"
}
]
} |
🔍 Wave 1 — Rails Conventions Review — WA-RCE-001 (Replace constantize with allowlists)Rails Conventions —
|
…ncern, remove lazy
Test Quality ReviewVerdict: PASS_WITH_NOTES Tests added for primary security behaviors (controller paths reject attacker-controlled class names with 422; AdminSearchQueryWrapper raises ArgumentError). Gaps are edge-case only:
None of these are blocking. The critical RCE paths are tested. |
Wave 2 Review — Databaseverdict: PASS
|
kitcommerce
left a comment
There was a problem hiding this comment.
Wave 3 reviews (performance / frontend / accessibility)
{"reviewer":"performance","verdict":"PASS_WITH_NOTES","severity":"LOW","summary":"Security allowlisting looks fine; minor overhead from constantizing allowlists per request is probably negligible but could be cached if needed.","findings":[{"severity":"LOW","file":"admin/app/controllers/workarea/admin/bulk_actions_controller.rb","line":42,"issue":"`Workarea.config.bulk_action_types.lazy.map { |t| t.constantize }` runs constantize on each request; if the allowlist grows, this could add avoidable overhead.","suggestion":"If this endpoint is hot, consider memoizing the resolved class list (e.g., pre-constantize in config or cache in a class attribute) while keeping the allowlist check."}]}{"reviewer":"frontend","verdict":"PASS","severity":null,"summary":"Controller/query changes only; no frontend code touched.","findings":[]}{"reviewer":"accessibility","verdict":"PASS","severity":null,"summary":"No accessibility-related UI changes.","findings":[]}
kitcommerce
left a comment
There was a problem hiding this comment.
Wave 4 (documentation) review
PASS — No documentation updates required beyond the code changes/tests already included.
Summary
Brakeman flagged three High-confidence Remote Code Execution warnings (UnsafeReflection) where user-controlled request parameters were passed directly to
.constantize. This PR replaces each unsafe call with an explicit allowlist lookup, eliminating all three findings.Fixes #802
Files Changed
admin/app/controllers/workarea/admin/bulk_actions_controller.rbBefore:
params[:type].constantizeAfter:
bulk_action_class_for(params[:type])— iteratesWorkarea.config.bulk_action_typesand matches by class name..constantizeis called only on allowlisted config strings.Allowlisted classes:
Workarea::BulkAction::DeletionWorkarea::BulkAction::ProductEditWorkarea::BulkAction::SequentialProductEditInvalid types →
422 Unprocessable Entity.admin/app/controllers/workarea/admin/create_segments_controller.rbadmin/app/controllers/workarea/admin/segment_rules_controller.rbBefore:
"Workarea::Segment::Rules::\#{params[:rule_type].to_s.camelize}".constantizeAfter:
segment_rule_class_for(params[:rule_type])— maps the slug to a class from the existingWorkarea.config.segment_rule_typeslist..constantizeis called only on the allowlisted strings.Allowlisted classes (from existing config):
BrowserInfo,Geolocation,Orders,Revenue,TrafficReferrer,Sessions,LastOrder,LoggedIn,TagsInvalid rule types →
422 Unprocessable Entity.core/app/queries/workarea/admin_search_query_wrapper.rbBefore:
params[:model_type].constantizeAfter: Validates
params[:model_type]against newWorkarea.config.admin_exportable_modelsallowlist before calling.constantize. RaisesArgumentErrorwith actionable message for unknown types.Allowlisted models (21 built-in, matching all usages in admin views):
Catalog::Category,Catalog::Product,Content::Asset,Content::Page,Email::Signup,Fulfillment::Sku,Inventory::Sku,Navigation::Redirect,Order,Payment::Transaction,Pricing::Discount,Pricing::Discount::CodeList,Pricing::Discount::GeneratedPromoCode,Pricing::Sku,Release,Segment,Shipping::Service,Shipping::Sku,Tax::Category,Tax::Rate,Usercore/lib/workarea/configuration.rbconfig.bulk_action_types(SwappableList) — plugin-safe, append withWorkarea.config.bulk_action_types << 'MyPlugin::BulkAction::Whatever'config.admin_exportable_models(SwappableList) — plugin-safe, append withWorkarea.config.admin_exportable_models << 'MyPlugin::MyModel'Tests
BulkActionsIntegrationTest#test_rejects_unknown_bulk_action_typetype: 'Kernel'→ 422, no BulkAction createdBulkActionsIntegrationTest#test_rejects_non_bulk_action_subclasstype: 'Workarea::User'→ 422 (not in allowlist)SegmentRulesIntegrationTest#test_rejects_unknown_rule_typerule_type: 'kernel'→ 422, no rule createdCreateSegmentsIntegrationTest#test_new_rule_rejects_unknown_rule_typeAdminSearchQueryWrapperTest#test_rejects_model_type_not_in_allowlistmodel_type: 'Kernel'→ ArgumentErrorAdminSearchQueryWrapperTest#test_rejects_blank_model_typeClient Impact
None expected for standard implementations.
Plugin authors who have added custom
BulkActionsubclasses, segment rule types, or exportable models must add those classes to the respective config lists in an initializer:This is consistent with the existing pattern for
segment_rule_types.Brakeman Verification
Scan target:
admin/(the engine containing the flagged controllers)Eliminated findings:
admin/.../bulk_actions_controller.rb:7— High confidenceadmin/.../create_segments_controller.rb:66— High confidenceadmin/.../segment_rules_controller.rb:59— High confidence