Skip to content

[OpenSpec] [openbuild] harden-xss-dos-csrf #1

Description

@rjzondervan

Security hardening from an attack-surface sweep of OpenBuild's own code. Fixes the exploitable items across three categories, leaving the already-defended surfaces untouched: two DoS Highs in the business-rules engine (unbounded FEEL parser/evaluator; no call-rule-set re-entry guard), a supply-chain-relevant CSRF hole on the settings controller (plus two smaller CSRF gaps), one cross-user XSS sink (the Docudesk document-template preview), and createFromTemplate DoS parity with the already-gated creation wizard.

Full enumeration lives in the change's attack-surface-map.md; rationale in design.md. The SSRF redirect fix (audit H2) was applied separately and is out of scope here.

Specs

  • business-rules-engine — bound the FEEL parser/evaluator (max length, recursion depth, AST node count); call-rule-set re-entry guard (depth + visited-slug); evaluate-payload size bound.
  • settings-and-observability — enforce CSRF on settings#create, settings#load, and preferences#setPreference.
  • openbuild-template-cataloguecreateFromTemplate gains the creation wizard's rate limit + authorization gate.
  • docudesk-document-templates — sanitize the document-template preview before render (DOMPurify).
  • app-icon-management — sanitize author-supplied SVG before preview and persistence (DOMPurify SVG profile).

Branch

Work on this branch — do NOT start work on development and do NOT create a different branch.

  • Base branch: development
  • Feature branch: feature/1/harden-xss-dos-csrf

Before starting:

git fetch origin
git checkout feature/1/harden-xss-dos-csrf 2>/dev/null || \
  git checkout -b feature/1/harden-xss-dos-csrf origin/development

Tasks

0. SSRF (audit H2) — folded in

  • 0.1 Disable redirect following (RemoteTemplateStoreService::fetch, allow_redirects => false)
  • 0.2 Test testFetchDisablesRedirectFollowing

1. DoS — bound the rule-evaluation stack

  • 1.1 FEEL parser length + recursion-depth cap (FeelParser::parse)
  • 1.2 Evaluator recursion-depth + AST-node cap (ExpressionEvaluator::evaluate)
  • 1.3 call-rule-set re-entry guard (depth + visited-slug, RuleEngineService::evaluate)
  • 1.4 Evaluate payload size bound + maskPii depth cap (RulesController::evaluate)
  • 1.5 Unit tests for the caps
    • over-length + over-depth expression rejected
    • self- and mutually-referential call-rule-set refused
    • oversized payload rejected before a RuleExecutionLog write

2. DoS — createFromTemplate parity

  • 2.1 Add #[UserRateLimit] + wizard authorization gate (ApplicationsController::createFromTemplate)
  • 2.2 Test: throttled excess calls; 403 for an unauthorized caller

3. CSRF — remove unjustified NoCSRFRequired

  • 3.1 Remove #[NoCSRFRequired] from SettingsController::create and ::load
  • 3.2 Remove @NoCSRFRequired docblock from PreferencesController::setPreference (keep @NoAdminRequired)
  • 3.3 Test: create/load/setPreference reject a missing token; SPA path still succeeds

4. XSS — sanitize the sinks

  • 4.1 Add dompurify dependency (pin; confirm SBOM/CI picks it up)
  • 4.2 Sanitize previewContent in DocumentTemplateAttachmentDialog.vue (full HTML profile)
  • 4.3 Sanitize the verbatim-SVG branch in iconCatalogues.js::resolveAppIcon (SVG profile)
  • 4.4 Vitest: injected <script>/onerror neutralized; benign markup/SVG preserved

5. Wrap-up

  • 5.1 Register this change in the five capability specs (OpenSpec changes list + status in-progress)
  • 5.2 Run Hydra mechanical gates + PHP/JS lint; confirm green

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions