"Who's going to attack me? I run a corner shop." We hear that a lot, and it makes sense: you picture someone choosing a target.
That isn't how it works. What's out there are programs sweeping thousands of sites a day, trying the same four things on every one of them. They don't know whether you're a hair salon or a bank, and they don't care. They're looking for an unlocked door.
And the part that really hurts isn't them getting in, it's what comes next. When Google spots a compromised site it flags it as deceptive: visitors get a full-screen red warning, and you disappear from search results. Getting back takes months, and meanwhile your competition takes your spot.
These are the four doors they come through and how to close them. The middle section is technical, so you can hand it to whoever looks after your site; the beginning and the end are for anyone.
Where to start: understand before testing
The temptation is to launch a scanner and see what comes out. That's the slowest way to audit and the one that produces the most false positives.
The first step is mapping the project:
- —Which forms exist, in which file, with which fields, and which endpoint they write to.
- —Which endpoints the backend has, with which HTTP method, which carry an auth guard, and which validate the request body.
- —Which frontend routes sound private: anything with "admin", "dashboard", "panel", "internal".
- —Whether a custom 404 page exists or the default one is being used.
With that map, the tests are targeted. Without it, you're firing blind at your own site.
1. Injection and XSS in forms
The risk is always the same: data a user typed ends up interpreted as code instead of treated as text. There are three variants and each one is fixed in a different place, so it's worth distinguishing them:
| Type | What happens | Where it's fixed |
|---|---|---|
| Reflected | The data comes back in the response unescaped | Backend output and rendering |
| Stored | It's saved to the database and appears later in another view | Input validation + output escaping |
| DOM-based | The frontend inserts raw user HTML | The component doing the inserting |
What to review in the frontend
React escapes content by default, and that covers most cases. The hole appears when someone deliberately bypasses that behaviour: dangerouslySetInnerHTML, direct innerHTML manipulation, or injecting HTML returned by an API.
The most reliable way to find it isn't testing from the outside, it's searching the code: a grep for those patterns gives you the complete list in seconds. For each occurrence, the question is whether the content can come from a user. If it can, either sanitise it with a dedicated library or drop the raw HTML — which is almost always the right call, because in most cases it wasn't needed.
What to review in the backend
That the DTOs validate for real. A DTO accepting any string of any length isn't validating anything. With class-validator in Nest.js: per-field rules (email format, maximum length, allowed pattern) and the global ValidationPipe with whitelist: true and forbidNonWhitelisted: true, so any undeclared field gets discarded instead of reaching business logic.
And one header that does a lot for very little: a restrictive Content-Security-Policy in the Next.js config. It doesn't replace sanitising input, but it turns many exploitable XSS cases into failed attempts.
2. Login: brute force and information leakage
This is the vulnerability we most often find wide open, because the login "works" perfectly. Four checks:
Is there an attempt limit? Without rate limiting, a bot can try passwords indefinitely. The test is trivial: six or ten attempts with the wrong password. If a 429 or a temporary lockout never appears, the fault is confirmed. You don't need a dictionary to prove it.
Does the error message distinguish cases? "This user doesn't exist" versus "incorrect password" is a useful distinction for the user and a gift to the attacker: it lets them enumerate which emails are registered without guessing a single password. The message must be the same in both cases: invalid credentials.
How are passwords stored? With bcrypt or argon2, always. Never in plain text, never with bare MD5 or SHA-1.
How is the session stored? If it's cookies: HttpOnly (so JavaScript can't read them), Secure (HTTPS only) and SameSite. If it's a JWT: the secret in environment variables — never in the repository — and a reasonable expiry.
The fix
In Nest.js, a throttler on the login endpoint with a limit on the order of five attempts per minute per IP and user, uniform error messages, and helmet for security headers if it isn't there already. That's half an afternoon of work and it closes the most common automated attack vector there is.
3. Exposed admin routes
An admin panel reachable without authentication is the most serious fault and also the easiest to check. What matters isn't whether the route exists, but what it returns to someone who hasn't logged in:
| Response | Verdict |
|---|---|
| 200 with real content | Critical |
| 401 / 403 | Correct |
| Redirect to login | Correct |
| Clean 404 | Correct |
The list of routes to probe comes from two places: the ones suspicious by convention (/admin, /dashboard, /panel, /api/docs, /swagger, /.env, /.git, /backup) and the project's real routes whose name suggests a private area. The second group is the one that matters, because those actually exist.
Three things that always get forgotten:
- —API documentation in production. A public Swagger is a complete map of your endpoints. In production, either protect it or disable it.
- —Accessible configuration files. A
.envor.gitserved by the web server is a direct leak of credentials and history. - —
robots.txtand the sitemap listing private routes. Even if the route is protected, you're publishing where to look. Blocking it withDisallowis correct, but remember that protects nothing: it only prevents indexing. The protection is the guard.
The fix is the missing authentication and role guard — in the backend on the controller, in the frontend in the middleware or in the private area's layout. And protect it on both sides: hiding the menu link isn't security, the route is still there.
4. The 404 page: security and SEO at once
A 404 looks like a design detail. It has two real implications.
Security: a default error page can leak technical information — the framework, the version, server paths, stack traces. That's free intelligence for anyone looking for a way in. A custom 404 reveals nothing.
SEO: links pointing at pages that no longer exist are lost if the visitor leaves. A 404 with the site navigation, a link home and maybe to the main services turns a dead end into a second chance. And it must return the real 404 status code, not a 200 with an error message: if it returns 200, Google indexes the error page as valid content.
In Next.js with the App Router a not-found.tsx file is enough, using the colours and logo the project already has. It's an hour of work, it shows up on every broken link for the life of the site, and there's no excuse for not having it.
How to prioritise the findings
Not everything an audit turns up carries the same weight:
| Severity | Examples | Timeframe |
|---|---|---|
| Critical | Admin panel with no auth, credentials in the repository, unhashed passwords | Immediately |
| High | Stored XSS, login without rate limiting, accessible .env | This week |
| Medium | Cookies without HttpOnly, no CSP, public Swagger, user enumeration | This month |
| Low | Default 404, informational headers, outdated dependencies with no known CVE | This quarter |
And something important about the report: if you document unfixed vulnerabilities in full detail, that document becomes as sensitive as the hole itself. Don't leave it in a public repository or a shared folder.
Continuous hygiene, not a one-off audit
An audit is a snapshot. What keeps a site secure is the routine:
- —Run
npm auditperiodically, and update anything with a known vulnerability. - —Secrets always in environment variables, never in code. And if one was ever committed, rotate it — Git history doesn't forget.
- —Backups somebody has restored at least once. A backup that has never been tested is an assumption, not a backup.
- —Repeat the four checks in this guide after every major change: a new form, a new endpoint, a change to the login.
Frequently asked questions
My site is small — is anyone really going to attack it? They're not going to attack you: they're going to attack everyone at once. The scans are automatic and indiscriminate. Being a small business doesn't lower the probability, it only lowers what the attacker gets if they're in — and even then, losing the site or dropping out of Google for a few weeks hurts plenty.
Doesn't an SSL certificate already protect me? HTTPS encrypts traffic between browser and server. It doesn't validate forms, doesn't limit login attempts and doesn't protect routes. It's necessary and it isn't sufficient.
How often should you audit? A full review each year, plus the specific checks after every relevant change. If there's a login or forms writing to a database, every six months is more sensible.
Does this slow the site down? Not perceptibly. Validating a form and checking an attempt limit are microsecond operations. If performance worries you, the problem is somewhere else: see how to score 100 on PageSpeed.
Conclusion
The four checks in this guide cover most of what an automated attacker tries against a business website: slipping code in through a form, hammering the login with attempts, walking in through a route nobody protected, and reading what an error reveals by accident.
None of them takes weeks. They take reviewing once, fixing what turns up, and checking again. And since security, performance and technical SEO are all audited against the same code, it makes sense to do them in one pass: the local SEO audit checklist covers the other front.
If you'd rather we reviewed it, we audit your site and hand you the findings by severity, with the fix applied in the code — not just pointed out in a PDF.
Do you want your business to appear on Google?
We discuss your business, your competition, and what you really need. No obligation. We'll respond in less than 48 hours with clear guidance and a fixed quote.
Schedule Free Consultation


