winter/wn-pwnedpasswords-plugin
Adds a Pwned Passwords validation rule to Winter CMS, checking passwords against the Have I Been Pwned breach database using k-anonymity so compromised credentials are rejected during registration and password resets.
Architecture fit. Winter CMS is a Laravel-based fork of October CMS; plugins are Composer packages that register a Plugin class and hook the framework boot lifecycle. A password-validation rule is a low-blast-radius extension point: additive, stateless, no schema change, no interception of the auth pipeline. The package's scope — one validation rule delegating to the HIBP Pwned Passwords range API — is deliberately narrow, which is the correct shape for a CMS plugin. The architectural question is not whether it fits, but whether the fit justifies a dependency at all.
The rule is roughly 60 lines of code. SHA-1 the candidate password, send the first five hex characters to api.pwnedpasswords.com/range/{prefix}, scan the returned suffix list for the remainder, compare the count against a threshold. That is the entire mechanism. A TPM should price the dependency against owning that function in-house, because build cost is hours and ownership cost is effectively zero.
Integration feasibility. High on the happy path. Install via Composer, run php artisan winter:up, enable in the backend. Enforcement points are limited and well-known: front-end registration and password reset (typically RainLab.User), backend user creation, and any custom model with a $rules array. Winter's Validator::extend() mechanism lets the rule attach alongside confirmed and min without modifying existing validation flows.
Technical risks.
Key questions before adoption.
Http facade/Guzzle, or raw file_get_contents? This determines testability, timeout control, and retry behaviour.Stack fit. Good, with the caveat that this is Winter-specific glue around a framework-agnostic idea. On Winter CMS with a Laravel-native stack, no translation layer is required. If the organisation is on October CMS upstream, plain Laravel, or a non-PHP platform, this package is the wrong artifact — the check belongs in a shared validation service or a Password::validate()-style rule in the application layer, not in a CMS plugin.
Migration path. There is no data migration; the rule is stateless and reversible by removing the validation string. The real migration is policy: from "accept any string meeting length and complexity" to "accept any string not present in a breach corpus." Sequence it:
Compatibility. Verify against the actual Winter build in use, not the README: Composer constraints, PHP floor, and whether the target install already extends Validator with a rule of the same name. Critically, confirm that RainLab.User (or whichever auth plugin is in place) routes password validation through a path the rule can attach to. If password setting happens in a custom controller that bypasses model validation, the rule silently never fires — the worst failure mode, because it looks like success.
Sequencing relative to other work. This is small and isolated and should neither block nor be blocked by other roadmap items. It does have a hard dependency on two non-engineering tasks that usually dominate the timeline: privacy review sign-off for the third-party egress, and a product decision on rejection threshold and user-facing copy. Start both before engineering, because a two-hour implementation behind a three-week legal review is a scheduling trap.
Maintenance and ownership. The dependency surface is tiny, which caps both the upside of adopting it and the downside of forking it. Given 5 stars and 0 dependents, the realistic bus-factor plan is: pin the version, vendor the plugin into the repository, and be prepared to take ownership of ~60 lines. Do not assume upstream responsiveness for a security-adjacent fix. The package has no schema, no background jobs, and no admin UI, so routine maintenance is near-zero — the operational cost lives entirely in the external dependency it wraps.
Support load. Expect a spike at enforcement time. Users whose long-standing password is rejected during an unrelated flow (password reset, profile edit) will read it as a bug or a lockout. Pre-write the error message to explain why — found in a breach corpus, not "too weak" — and offer a path forward. Without that, every rejection becomes a ticket.
Scaling characteristics. Volume is bounded by password-set events, not logins or page views, so throughput is genuinely low; this is not a scaling problem in the ordinary sense. The two real concerns are: (a) added latency on a user-facing write path, mitigated by a short timeout and, if needed, a prefix cache; and (b) bursts during bulk resets, imports, or test seeding, which should bypass the rule or be queued rather than hammering HIBP. If the organisation ever needs thousands of evaluations per minute, the API is the wrong substrate and the offline corpus is the answer.
Failure modes and required mitigations.
Password123) is actually rejected in a smoke test.Observability. Instrument the rule as a discrete unit: evaluation count, hit rate, p50/p95 latency, error and timeout rate, and fail-open count. The fail-open counter is the one that matters — it silently tells you the gap between the protection you believe you have and the protection you actually have.
Ramp-up. For a Laravel developer, an afternoon including the smoke tests above. The long poles are the privacy review and the product decision on messaging and thresholds. Given the low overall score (16.8) and low opportunity signal (12.8), the honest prioritisation framing is: this is a cheap, credible security-hygiene improvement with a real but modest UX cost — worth doing once, if and only if the fail-open policy and observability are in place before enforcement is switched on.
How can I help you explore Laravel packages today?