Merged
Conversation
…mfa and login endpoints
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 / -1206across 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, storesSHA-256(plaintext)inuser.password_reset_hash, and carries the plaintext on a transientUser::$plaintextPasswordResetHash(not persisted, marked#[Ignore]).UserRepository::loadUserByPasswordResetHash()hashes the URL token before lookup.MailingService::sendImmediate()sends synchronously and persists aMailingaudit row with'[redacted]'body — no rendered link ever touches the DB.LoginControllerandUserService::welcomeUserusesendImmediate()instead of the queue-basedadd().Upload service allowlist (F05)
App\Service\UploadService::upload()now triple-gates every upload:DEFAULT_ALLOWED_MIME_TYPESDEFAULT_ALLOWED_EXTENSIONSHTML, SVG, and any executable content are rejected by default. Throws
App\Exception\FileExtensionNotAllowedExceptionon 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::loadPhpExcelObjectnow usesIOFactory::createReaderForFile($f); $reader->setReadDataOnly(true);instead ofIOFactory::load.SammToolboxImporterServiceswitches togetValue()on the validation-remarks fields so injected formulas are not evaluated server-side.Content-Disposition injection (D01)
ReportingController::exportSammJsonpreviously didsprintf('attachment; filename="%s"', $filename), allowing header injection through project name. Now usesHeaderUtils::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::exportSammandexportSammJsonnow calldenyAccessUnlessGranted('PROJECT_ACCESS', $project)before doing any work.Sensitive exception text no longer leaks to clients (F09)
CrudAjaxModifyTrait::abstractAjaxModifypreviously 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 injectedLoggerInterfaceand returns a generic message to the client.CSPRNG failure no longer silently produces empty tokens (F11)
RandomStringGenerator::base32/base64previously caught\Exceptionfromrandom_bytes()and returned''. After the F02 changes, an empty plaintext would have been stored asSHA-256('')(a well-known constant), making any CSPRNG failure exploitable. The catch is removed; failures propagate up toResetPasswordService::reset()'s outer try/catch, which already returnsfalseand logs.Login + MFA + password-reset rate limiting
(From the prior
85e387acommit, included in this branch.) Symfony rate limiter replaces the oldBruteForceService/FailedLogintable:LoginRateLimitSubscriber)MfaRateLimitSubscriber)PasswordResetRateLimitSubscriber)Migration
Version20260323202159drops thefailedlogintable and theuser.failed_loginscolumn.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 inDocumentationControllerTest.AbstractRepository::getPaginatedList(),getSearchResult(), and the now-orphanedgenerateSearchQuery()helper.App\Util\IndexViewParameters(entire class),App\Util\RepositoryParameters(entire class).RepositoryParameterssetup block inStageController::ajaxindexForAssignment.Production bug fix found while running tests
ProjectRepository::deepRestorecalled$assessmentRepository->findOneBy(['project' => $project])onAssessment.Assessment#projectis the inverse side of the relation — Doctrine ORM 3 hard-rejects this withInvalidFindByCall. 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 storesSHA-256(plaintext)and passes plaintext to the query.tests/Service/ResetPasswordServiceTest.php::testPasswordResetLinkValidity— assertion bumped from 12h to 1h.tests/functional/LoginControllerTest.php::performResetPasswordFlowandtestSsoLinkExpiration— driveResetPasswordService::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 theStubMailingServicebelow.Test infrastructure additions:
tests/_support/StubMailingService.php(new) — extendsMailingServiceand overridessendMail()to returntruein tests, sosendImmediate()no longer needs real SMTP. Wired inconfig/services_test.yamlto replace the productionMailingService.config/packages/test/framework.yaml— pins the rate-limiter cache tocache.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 (
MaturityLevelproxy, missingtoolbox-2.0v.xlsx, stream-weight data shape) not introduced by these changes.Deployment notes
Version20260323202159dropsfailedlogintable anduser.failed_loginscolumn. Down migration recreates them for rollback.Follow-ups (not in this PR)
Production timezone bug —
ResetPasswordService::reset()writes\DateTimewithout explicit TZ; Doctrine'sdatetimetype 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 inconfig/packages/doctrine.yaml:Worked accidentally before F02 because
+8 hoursabsorbed the skew. Affects any deployment where MariaDB defaults to local time. Recommend adding before the next prod release.UserRepository::loadAdminByPasswordResetHashis now orphaned (zero callers after the SHA-256 changes). Cleanup candidate.UserSubscriber::onUserCreatedignores thewelcomeUserreturn value. WithsendImmediate, 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 withresendWelcomematters.