The project is a refactoring of Protected Pages contrib module which is considered feature complete despite multiple feature request and contribution proposal. Additionally Protected Pages is an old module carried over through the various Drupal versions without a proper refactoring on most recent standards.
So I decided to implement Protected Pages Extra, targeting only Drupal 11 in order to adopt HTTP Middlware and Configuration entity, and provide to the community a module that fulfill the same purpose of Protected Pages but with an open mind to improvements.
The module has been implemented initially using the support of Claude code but then heavely review and refactored because all the nonsense put in by Claude code.
The readme file contains an extensive description of the module purpose, the difference with protected pages and the upgrade path.
Comments
Comment #2
vishal.kadamComment #3
avpadernoThank you for applying!
Before giving links helpful to understand how the review process works, what to expect from a review, and what to do to avoid a review takes more time than needed, I would like to thank all the reviewers for the work they do.
These applications are volunters-driven, which also means it is not possible to predict when an application will be marked fixed and the applicant will get the permission to opt projects into security advisory policy. While we aim to make an application as quick as possible, it is also important for us that more people review the project used for an application. In this way, we make sure applications do not miss some important points that should be instead reported.
Applications are not meant to be complete debugging sessions that eliminate every existing bug, though. I apologize if sometimes applications seem to go into too-detailed reviews.
Please read Review process for security advisory coverage: What to expect for more details and Security advisory coverage application checklist to understand what reviewers look for. Tips for ensuring a smooth review gives some hints for a smoother review. See also Policy on the use of AI when contributing to Drupal, which is valid when contributing to Drupal, either by committing code in a project, or by creating a merge request for an existing project.
The important notes are the following.
Keep in mind that once the project is opted into security advisory coverage, only Security Team members may change coverage.
To the reviewers
Please read How to review security advisory coverage applications, Application workflow, What to cover in an application review, and Tools to use for reviews.
The important notes are the following.
For new reviewers, I would also suggest to first read In which way the issue queue for coverage applications is different from other project queues.
Comment #4
abdelwahiedReviewed 1.0.x (commit 816ef97) on Drupal 11.
AUTOMATED REVIEW
phpcs (Drupal + DrupalPractice) across php/module/install/yml/twig: 0 errors, 0 warnings. The repo also ships its own phpcs.xml.dist, phpstan.neon.dist and phpunit.xml.dist — good to see.
MANUAL REVIEW — POSITIVES
- Full dependency injection; no \Drupal:: static calls anywhere in src/.
- Clean, modern architecture: HTTP StackMiddleware, a ProtectedPage config entity with a list builder and access checker, a service-provider override and cache/event subscribers — a proper replacement for the original procedural approach.
- Tests present (6 test files), README included, correct core_version_requirement (^11.1), and no hardcoded version/project keys in the .info.yml.
NOTES / QUESTIONS
- Since this is positioned as a refactor of the existing Protected Pages module, a short paragraph in the README (and on the project page) about the relationship and migration path from Protected Pages would help users and reviewers understand the scope.
This is a strong, clean application from a coding-standards and security standpoint. Nicely done.
Comment #5
avpadernoComment #6
marco.pagliarulo commentedThe readme file contains a full documented section about the migration https://git.drupalcode.org/project/protected_pages_extra#migrating-from-... covering the migration from protected_pages table to the new entities, the settings mapping the permission mapping, and the decorator solution adopted to let the two modules live alongside each other without prompting the user twice for the same password, so the site admin can verify that the migration is successful before disabling the legacy module.
I added a link to the readme section inside the module page.
Comment #7
hsjbrianwillows commentedReviewed
1.0.xat commit816ef97.Automated checks
PHPCS (Drupal, DrupalPractice) across 32 files: 0 errors, 0 warnings, which matches abdelwahied's result in #4.
PHPStan with phpstan-drupal at the declared level 1 is clean apart from the
ProtectedPagesSubscriberOverrideartefacts your own config already excludes. At level 5 the production code produces exactly one hit, noted below.Findings
1. Path prefix checks have no boundary
ProtectedPagesExtraMiddleware::handle()line 60 andProtectedPagesExtraCacheSubscriber::onResponse()line 71 both usestr_starts_with($path, '/admin').ProtectedPageForm::validateForm()lines 279 and 284 do the same for/adminand for the login path.Paths like
/admin-guide,/administratorsor/admin-toolssatisfy that test without being admin paths, and/protected-page/login-helpsatisfies the login-path test. In the form this rejects legitimate paths. In the middleware and the response subscriber it skips the protection check.Form validation is not a security boundary here. A
protected_pageentity created by config import, by an update hook, or in code never passes throughvalidateForm(), and the schema regex allows such a path, so an entity of that shape would be silently unprotected at request time.Suggested:
$path === '/admin' || str_starts_with($path, '/admin/'), and the same treatment for the login path.2. No flood control on password attempts
Nothing in the module uses the
floodservice. Neither the login form nor the?password=check in the middleware limits guesses, per IP or per entity. The append-password mode makes brute forcing cheap, since it needs only plain GET requests with no form token and no session. Core's user module (thefloodservice plususer.floodconfig) is the obvious model. For a module whose entire job is a password gate, throttling plus a logged warning would be a real improvement.3. matchPath() gives up on the first wildcard whose entity fails to load
ProtectedPagesExtraAccessChecker::matchPath()lines 143 to 149: inside theforeach, a matching pattern doesreturn $entity instanceof ProtectedPageInterface ? $entity : NULL;. A load failure returns NULL immediately instead of continuing to the remaining patterns, so a later valid pattern is never consulted and the path is treated as unprotected. It is unlikely to fire, sincewildcardIndex()only records ids it already loaded successfully, but the failure direction is open rather than closed.continuewould be safer.4. Unreachable branch in the migration
protected_pages_extra.installline 48:ProtectedPage::load($id)can never return non-NULL, because_protected_pages_extra_unique_id()already loops until the id is unused. So$skippednever increments and the migration message always reports "0 already existed".Related, and what PHPStan flags at level 5:
$changedin_protected_pages_extra_migrate_settings()is set TRUE at line 96 and never set FALSE, so theif (!$changed)at line 134 is dead code.5. The append-password option deserves a warning
The checkbox description says what the option does but not what it costs. A password in a query string ends up in web server access logs, proxy logs, browser history, and Referer headers on outbound links. Your README security notes are thorough enough that this stands out by its absence.
6. Tooling
phpstan.neon.distdeclares level 1. At level 5 the production code produces only finding 4, so raising it looks cheap.The
@phpstan-ignore bbd.naming.propertyNotCamelCaseonProtectedPage::$allow_append_passwordtargets a sniff that lives in a private Composer repository, andreportUnmatchedIgnoredErrors: falseis set project-wide to accommodate it. That weakens PHPStan for everyone else working on the module. The property name has to match the config key, which is ordinary for a config entity, and no public ruleset objects to it.Two test-only type errors at level 5:
SettingsConfigTranslationTestline 53 callsgetLanguageConfigOverride()onLanguageManager(type againstConfigurableLanguageManagerInterface, or uselanguage.config_factory_override), and line 96 callstoArray()onDataDefinitionInterface.Things I checked and found correct
hook_ENTITY_TYPE_insertand_updatecalls toCache::invalidateTags(['http_response'])are what close that, andhttp_responseis core's own bulk lever for exactly this. page_cache's request policy also skips requests carrying a session, so unlocked visitors never populate it. I also checked whether a delete counterpart was missing and concluded it is not needed, since protected responses carryno-storeand the login redirect is a plainRedirectResponsethatPageCache::storeResponse()refuses.ProtectedPagesLoginForm::submitForm()is sound. Prepending the slash afterltrim()neutralises protocol-relative URLs andUrlHelper::isExternal()catches absolute ones.PasswordInterface, blank preserves the existing hash on both forms, and the migration copies the legacy hash rather than re-hashing it, so migrated passwords keep working.userCanBypass()readinguidstraight from the session mirrors what core's ownSessionHandlerdoes, so that is not a defect.Summary
A strong candidate. The architecture is modern and consistent (config entity, hook attributes, dependency injection throughout,
FullyValidatableschema constraints,ConfigTargeton the settings form), and the README is better than most contrib modules ever manage, particularly the caching and config-export sections.I would want findings 1 and 2 addressed before coverage. The rest are minor.
I use Assisted AI but I review what it generates
Comment #8
marco.pagliarulo commentedHi hsjbrianwillows,
thanks for this great review. For flood control there is already a MR created and one of my coworker is reviewing it. It will be available soon.
I'll work on the other tasks ASAP.
Comment #9
marco.pagliarulo commentedHi hsjbrianwillows,
thanks for all the great feedbacks, I just merged some improvements and I am going to create a new release soon.
1. Path prefix checks have no boundary
This implementation was really sloppy, I rushed a little bit on that matter probably. Still I didn't like to rely on str_starts_with, so I totally changed approach. Since the middleware run when the routing has not been yet completed, I retrieve the route candidates which are already available and check how many are marked as _admin_route and how many not, If there are 0 candidates without _admin_route and at least 1 candidate with _admin_route, then the path will surely resolve in an admin path.
This gave the advantage to check also admin path that do not start with admin/ for example node/1/edit or any other possible contrib admin route not falling under /admin/*
There is still a scenario where there are candidates for admin and not admin paths, which get verified, because potentially the requested route is one of the non admin, but at this stage there is no way to know, so I let it check it considering that this is edge case and anyway it was even with the str_starts_with approach (which edge cases where even wider).
For the cache subscriber I directly used adminContext->isAdminRoute since at that point the routing has already happened; same for the config form.
2. No flood control on password attempts
Implemented for both the login by form and by append-password. I actually didn't used user.flood, but implement the module's specific configuration.
5. The append-password option deserves a warning
I totally agree, I am not actually really fond of this feature, but I had to implement it. Now the flag has a security note in the admin form.
And the readme says now
6. Tooling
Changed the phpstan version to 5 (it is the one I prefer) and addressed the coding standard issues on test.
I am going to address 3 and 4 as well.
Comment #10
avpadernoAs a side note, these applications do not require creating a new release. It is better to work on a branch without tagging new releases.
Comment #11
marco.pagliarulo commentedAlso 3 and 4 addressed. Please review again.
Comment #12
marco.pagliarulo commentedComment #13
marco.pagliarulo commentedI understand the rationale for this, but in this case I considered too important to provide those security features ASAP to whoever adopt the module.