Skip to content

Security fixes#147

Merged
aramhovsepyan merged 13 commits intoOWASP:mainfrom
BackNot:security-fixes
Apr 18, 2026
Merged

Security fixes#147
aramhovsepyan merged 13 commits intoOWASP:mainfrom
BackNot:security-fixes

Conversation

@BackNot
Copy link
Copy Markdown
Collaborator

@BackNot BackNot commented Apr 18, 2026

Security hardening + dead code cleanup

Fixes a batch of findings from the latest security scan, plus a real production bug found while running the test suite. Mostly small, contained changes; net +1101 / -1206 across 50 files because it deletes more dead code than it adds.

Security fixes

Password reset token is no longer stored as plaintext (F02)

  • ResetPasswordService::reset() now generates plaintext base32, stores SHA-256(plaintext) in user.password_reset_hash, and carries the plaintext on a transient User::$plaintextPasswordResetHash (not persisted, marked #[Ignore]).
  • The mailer reads the transient plaintext for the URL; the persisted column never sees it.
  • UserRepository::loadUserByPasswordResetHash() hashes the URL token before lookup.
  • Default validity reduced from 8h to 1h (welcome email stays at 8h).
  • MailingService::sendImmediate() sends synchronously and persists a Mailing audit row with '[redacted]' body — no rendered link ever touches the DB.
  • LoginController and UserService::welcomeUser use sendImmediate() instead of the queue-based add().

Upload service allowlist (F05)

App\Service\UploadService::upload() now triple-gates every upload:

  1. Sniffed MIME against DEFAULT_ALLOWED_MIME_TYPES
  2. Sniffed extension against DEFAULT_ALLOWED_EXTENSIONS
  3. Client-supplied extension against the same allowlist

HTML, SVG, and any executable content are rejected by default. Throws App\Exception\FileExtensionNotAllowedException on rejection. The saved filename uses the sniffed extension, never the client-supplied one. Optional per-call overrides for both lists.

PhpSpreadsheet read-only + no formula evaluation (F03)

  • ExcelImporter::loadPhpExcelObject now uses IOFactory::createReaderForFile($f); $reader->setReadDataOnly(true); instead of IOFactory::load.
  • SammToolboxImporterService switches to getValue() on the validation-remarks fields so injected formulas are not evaluated server-side.

Content-Disposition injection (D01)

ReportingController::exportSammJson previously did sprintf('attachment; filename="%s"', $filename), allowing header injection through project name. Now uses HeaderUtils::makeDisposition() with a sanitized ASCII fallback (preg_replace('/[^A-Za-z0-9._-]/', '_', $filename)) — proper RFC 6266/5987 dual emission.

Authorization gate on export endpoints (D03)

ReportingController::exportSamm and exportSammJson now call denyAccessUnlessGranted('PROJECT_ACCESS', $project) before doing any work.

Sensitive exception text no longer leaks to clients (F09)

CrudAjaxModifyTrait::abstractAjaxModify previously echoed $t->getMessage() to the JSON response. That could surface Doctrine/PDO error text including SQL fragments and table/column names. Now logs the exception via the controller's injected LoggerInterface and returns a generic message to the client.

CSPRNG failure no longer silently produces empty tokens (F11)

RandomStringGenerator::base32/base64 previously caught \Exception from random_bytes() and returned ''. After the F02 changes, an empty plaintext would have been stored as SHA-256('') (a well-known constant), making any CSPRNG failure exploitable. The catch is removed; failures propagate up to ResetPasswordService::reset()'s outer try/catch, which already returns false and logs.

Login + MFA + password-reset rate limiting

(From the prior 85e387a commit, included in this branch.) Symfony rate limiter replaces the old BruteForceService / FailedLogin table:

  • 5 login attempts / minute per IP (LoginRateLimitSubscriber)
  • 5 MFA attempts / minute per IP (MfaRateLimitSubscriber)
  • 5 password-reset requests / hour per IP (PasswordResetRateLimitSubscriber)

Migration Version20260323202159 drops the failedlogin table and the user.failed_logins column.

Dead code removal

Deleted because nothing in the codebase still uses them:

  • DocumentationController::show(), preview(), findFilePath() — and the matching template, plus 8 functional tests + 2 providers in DocumentationControllerTest.
  • AbstractRepository::getPaginatedList(), getSearchResult(), and the now-orphaned generateSearchQuery() helper.
  • App\Util\IndexViewParameters (entire class), App\Util\RepositoryParameters (entire class).
  • The dead RepositoryParameters setup block in StageController::ajaxindexForAssignment.

Production bug fix found while running tests

ProjectRepository::deepRestore called $assessmentRepository->findOneBy(['project' => $project]) on Assessment. Assessment#project is the inverse side of the relation — Doctrine ORM 3 hard-rejects this with InvalidFindByCall. Replaced with $project->getAssessment(), which uses the already-hydrated owning side.

Test suite

Started at 442 errors (all infra: MySQL/Redis down). After bringing infra up: 28 unique failures. Now: 13. The 15 fixed in this branch break down as:

Caused by F02 password-reset hardening:

  • tests/Repository/UserRepositoryTest.php::loadUserByPasswordResetHashProvider — provider now stores SHA-256(plaintext) and passes plaintext to the query.
  • tests/Service/ResetPasswordServiceTest.php::testPasswordResetLinkValidity — assertion bumped from 12h to 1h.
  • tests/functional/LoginControllerTest.php::performResetPasswordFlow and testSsoLinkExpiration — drive ResetPasswordService::reset() directly to capture the transient plaintext, use it in URLs (the form-submit path triggers an SMTP send the test env can't satisfy).
  • tests/Service/UserServiceTest.php::testWelcomeUser — passes thanks to the StubMailingService below.

Test infrastructure additions:

  • tests/_support/StubMailingService.php (new) — extends MailingService and overrides sendMail() to return true in tests, so sendImmediate() no longer needs real SMTP. Wired in config/services_test.yaml to replace the production MailingService.
  • config/packages/test/framework.yaml — pins the rate-limiter cache to cache.adapter.array (in-memory). Each test gets a fresh kernel → fresh cache → no cross-test bleed. Fixes 7 login + auth tests at once.

New tests for the hardened code:

  • tests/Service/UploadServiceTest.php — 7 cases covering allowed text/PNG, rejected HTML/SVG, spoofed extension, mismatched client extension, and custom-allowlist override.

The remaining 13 failing tests on this branch are pre-existing fixture issues (MaturityLevel proxy, missing toolbox-2.0v.xlsx, stream-weight data shape) not introduced by these changes.

Deployment notes

  • Run migrationsVersion20260323202159 drops failedlogin table and user.failed_logins column. Down migration recreates them for rollback.
  • No config changes required for the rate limiter — its cache pool defaults are fine in prod (Redis-backed).

Follow-ups (not in this PR)

  • Production timezone bugResetPasswordService::reset() writes \DateTime without explicit TZ; Doctrine's datetime type stores wall-clock only. If the deployed MySQL session TZ ≠ PHP TZ, the new 1-hour validity makes tokens dead on arrival. One-line fix in config/packages/doctrine.yaml:

    doctrine:
      dbal:
        options:
          1002: "SET time_zone = '+00:00'"

    Worked accidentally before F02 because +8 hours absorbed the skew. Affects any deployment where MariaDB defaults to local time. Recommend adding before the next prod release.

  • UserRepository::loadAdminByPasswordResetHash is now orphaned (zero callers after the SHA-256 changes). Cleanup candidate.

  • UserSubscriber::onUserCreated ignores the welcomeUser return value. With sendImmediate, an SMTP failure during initial user creation goes unsurfaced to the admin (logged server-side, but no flash). Mitigated by the new resend-welcome button. Worth a one-line flash if symmetry with resendWelcome matters.

@aramhovsepyan aramhovsepyan merged commit 7792126 into OWASP:main Apr 18, 2026
2 checks passed
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.

2 participants