Login forms: harden the page attackers try first
A discovered login isn't a flaw; leaving it without rate-limiting, MFA, and secure cookies is
The threat
This one is informational: nothing is broken. The scanner simply found a login, a page with a password field, which is the front door to your accounts. We flag it because of where it sits, not because of a bug. It’s the one place an attacker can reach out and rattle the handle, so it’s worth confirming it’s ready for the attention it will get.
A public login is the single spot where someone can try credentials directly, no exploit required. Automated tools throw known username and password pairs at it (credential stuffing) and hammer common passwords (brute force) around the clock. None of that is a flaw in your code, it’s just traffic, which is exactly why the defenses have to be deliberate.
The exact fix
This is defense in layers, not a one-line patch. Work down the list:
Rate-limit and slow down guessing. Cap attempts per IP and per account, add a short lockout or exponential backoff after a handful of failures, and put a challenge (CAPTCHA or similar) in front of repeated misses. At the edge:
# nginx: throttle the login endpoint
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
location = /login {
limit_req zone=login burst=5 nodelay;
# ... proxy_pass to your app
}
Turn on MFA. A second factor (an authenticator app or TOTP, a hardware key, at minimum email or SMS) means a correct-but-stolen password still isn’t enough. It’s the biggest lift for the least code, especially on admin accounts.
Harden the session cookie the login hands out:
Set-Cookie: __Host-session=...; Secure; HttpOnly; SameSite=Lax; Path=/
Secure: only sent over https.HttpOnly: JavaScript can’t read it, so an XSSXSSAttackCross-Site Scripting: an attack that injects attacker-controlled JavaScript into your page to steal sessions, keylog, or act as the user. A strong CSP is the primary defense. can’t steal it.SameSite=Lax: it won’t ride along on cross-site requests.
Keep error text generic. Return “Invalid email or password” for every failure, never “no such user” versus “wrong password.” Different messages (or noticeably different response times) let an attacker enumerate which accounts exist. Apply the same rule to the forgot-password flow.
Put it behind the same hardened headers as the rest of the site: HTTPS-only, HSTSHSTSHTTP headerHTTP Strict Transport Security: a header that forces browsers to always use HTTPS for your domain, defeating downgrade and SSL-strip attacks., a strong CSPCSPHTTP headerContent-Security-Policy: a response header that whitelists exactly which scripts, styles, images and connections a page may use, so an injected payload simply has no permission to run., and frame-ancestors 'none' so the form can’t be framed for clickjackingclickjackingAttackAn attack that invisibly frames your real site over a decoy, so victims click genuine buttons without knowing. Blocked by frame-ancestors or X-Frame-Options..
Do not answer this finding by hiding the login. An admin panel moved to
/secret-adminis still findable, and now it’s just untracked. The goal is a login that shrugs off attention, not one you’re hoping nobody notices.
Verify it
curl -sI https://yourdomain.com/login | grep -iE 'strict-transport|content-security'
You want the login served over HTTPS with HSTS and a CSP that includes frame-ancestors 'none'. Then log in and check the session cookie in your browser’s DevTools (Application, then Cookies): Secure, HttpOnly, and SameSite should all be set. Finally, try a few wrong passwords in a row and confirm attempts get throttled or challenged rather than accepted endlessly.
Proof
Because it’s informational, this is the lightest line on your report (−1), and rightly so: a login isn’t a weakness, an un-hardened one is. The ApeCyber scanner catalogues it passively, from the outside, it only sees that a password field exists, touching nothing. apecyber.com and dev3lop.com grade A / 100 with hardened, HTTPS-only headers on every page, the same baseline every login should sit behind.