You run a security scan.No critical vulnerabilities. No outdated libraries. No obvious SQL injection. Your WAF is enabled. HTTPS is working. Everything looks fine.
Then someone logs into your website and starts seeing data that belongs to another user.
There was no zero-day.
No fancy exploit.
No dramatic SQL injection payload.
The application simply trusted the wrong thing.
This is one of the easiest security problems to misunderstand.
A website doesn't need a known software vulnerability to be hacked.
Sometimes the problem isn't a broken piece of software.
The problem is that the software is doing something you never intended it to do.
And that's where website security gets interesting.
A Security Scan Can Say "No Vulnerabilities" and You Can Still Have a Problem
Let's say you built a SaaS application.
A user logs in and gets an account ID:
https://example.com/account/1842
The application works perfectly.
Authentication works.
Passwords are hashed.
HTTPS is enabled.
Your dependencies are up to date.
A scanner might find nothing obviously wrong.
Now imagine the user changes the URL:
https://example.com/account/1843
And suddenly they can see somebody else's account.
That's not necessarily a vulnerable library.
It's an authorization problem.
The application authenticated the user correctly.
It just failed to answer the next question:
> Is this user allowed to access this specific account?
This class of problem is known as Broken Access Control, and OWASP currently ranks it #1 in the 2025 Top 10. In OWASP's contributed testing data, every application tested had some form of broken access control, with an average incidence of 3.74%.
That's a pretty good reminder that security isn't just about finding CVEs.
Your application can be running the latest version of everything and still make a terrible security decision.
Your Login Can Work Perfectly and Your Security Can Still Fail
Authentication and authorization sound similar, but they're not.
Authentication asks:
> Who are you?
Authorization asks:
> What are you allowed to do?
Imagine I'm a normal user.
I successfully log in.
The server gives me a valid session cookie.
So far, everything is fine.
Now I send:
GET /api/invoices/1043
Cookie: session=abc123
The server checks my session.
Valid.
It returns the invoice.
But what if invoice 1043 belongs to another customer?
That's the actual security problem.
The attacker doesn't need to steal a password.
They don't need to bypass login.
They already have a valid account.
They just need the application to forget one small check:
Does this invoice belong to this user?
OWASP specifically calls out this type of identifier tampering and BOLA-style access as a Broken Access Control problem. Their recommended approach is to enforce authorization in trusted server-side code and verify resource ownership rather than trusting identifiers supplied by the client.
This is also why a WAF can't solve everything.
A request like this looks completely normal:
GET /api/invoices/1043
Authorization: Bearer <valid-token>
The WAF sees a normal authenticated API request.
Your application knows whether invoice 1043 belongs to the current user.
The security decision belongs there.
The Frontend Is Not a Security Boundary
This one catches developers all the time, especially when building modern JavaScript applications.
You hide an admin button:
if (user.role === "admin") {
showDeleteButton();
}
Looks fine.
Except the browser belongs to the user.
They can modify the JavaScript.
They can call the API directly.
They can use curl, Burp Suite, or another HTTP client.
If your backend has:
DELETE /api/users/4821
the server must independently verify that the current user is allowed to delete that account.
Hiding the button doesn't protect the endpoint.
OWASP gives essentially this exact warning: if access control exists only in the frontend, an attacker can directly request the backend endpoint instead.
A good rule is:
> If the browser can make the request, assume the user can make the request without your UI.
The backend has to enforce the rule.
Every time.
Sometimes the Problem Is Just a Bad Configuration
Not every attack starts inside your application code.
Sometimes someone simply left something exposed.
For example:
/admin
/test
/debug
/staging
/backup
Or a server accidentally exposes:
/.git/
.env
backup.zip
database.sql
Maybe directory listing is enabled.
Maybe an old staging server is publicly accessible.
Maybe a debug endpoint returns internal information.
Maybe an S3 bucket or cloud storage location has the wrong permissions.
There doesn't have to be a complicated exploit.
The attacker finds something you accidentally made public.
That's why OWASP's 2025 Top 10 puts Security Misconfiguration at #2. OWASP notes that misconfiguration problems have become more prevalent as applications increasingly depend on configuration for their behavior.
Here's a simple example.
Your application should return:
HTTP/1.1 404 Not Found
for:
https://example.com/.env
But instead it returns:
HTTP/1.1 200 OK
Content-Type: text/plain
with:
DATABASE_URL=postgres://...
STRIPE_SECRET_KEY=...
JWT_SECRET=...
That's not a sophisticated vulnerability.
You just exposed your secrets.
And if those credentials are real, the attacker may not need to attack your website anymore.
They can attack the services those credentials belong to.
Leaked Secrets Can Turn Into a Full Compromise
This is one of my least favorite mistakes because the original problem can be tiny.
Someone accidentally commits:
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
to a Git repository.
Or puts an API key into frontend JavaScript.
Or uploads a .env file.
Or stores credentials in a public backup.
Or logs a token.
Then somebody finds it.
Now imagine that leaked credential has permission to:
- Read customer files
- Upload objects
- Access a database
- Send email
- Create cloud resources
- Read application logs
The original mistake was "a secret was exposed."
The consequence can be much larger.
This is also why simply deleting the secret from the current version of a repository isn't always enough. If it was committed previously, it may still exist in Git history.
You need to rotate the credential.
That's the difference between removing the evidence and removing the attacker's access.
Business Logic Bugs Are Even Stranger
Here's a simple example.
Your store gives each customer one $20 discount.
The developer creates an endpoint:
POST /api/apply-coupon
{
"coupon": "WELCOME20"
}
The endpoint works.
Authentication works.
Authorization works.
Input validation works.
No SQL injection.
No XSS.
No known vulnerable dependency.
But the developer forgot to enforce:
One use per customer
So the attacker sends the request repeatedly.
WELCOME20
WELCOME20
WELCOME20
WELCOME20
WELCOME20
The application keeps accepting it.
Nothing about those requests looks malicious.
The attacker is simply abusing the business rules.
OWASP's separate Business Logic Abuse project specifically covers problems such as missing transition validation, resource quota violations, artifact lifetime issues, and broken access control.
This is why security testing can't stop at:
> "Can I inject something?"
Sometimes the better question is:
> "Can I make the application do something it was never supposed to allow?"
A Stolen Session Doesn't Need a Vulnerable Server
Here's another uncomfortable scenario.
Your server is perfectly patched.
Your WAF is working.
Your application has no known critical CVE.
But an attacker gets hold of a user's session cookie.
Now they don't need your login page.
They already have an authenticated session.
Depending on how the application handles sessions, that could give them access to the user's account.
This is why session security matters:
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Lax
Secure helps ensure the cookie is sent over HTTPS.
HttpOnly prevents normal JavaScript from reading it.
SameSite helps reduce certain cross-site request risks.
But even these flags don't magically solve account takeover.
You still need sensible session expiration, server-side invalidation where appropriate, secure authentication flows, and protection around password resets and account recovery.
OWASP recommends invalidating stateful sessions after logout and keeping stateless JWT lifetimes short enough to reduce the window available to an attacker.
So What Should You Actually Check?
If I had to review a website quickly, I wouldn't start by asking only:
> "What CVEs does this website have?"
I'd ask a wider set of questions.
CheckWhat you're looking forAuthenticationCan accounts be taken over or bypassed?AuthorizationCan users access someone else's data?API endpointsAre POST, PUT, DELETE properly protected?Admin panelsAre privileged functions actually restricted?ConfigurationAre debug pages, backups, or directories exposed?SecretsAre API keys, tokens, or credentials exposed?SessionsAre cookies and tokens handled safely?Business logicCan normal features be abused in unintended ways?DependenciesAre known vulnerable components running?MonitoringWould you know if someone started abusing the app?
And I'd test with more than one account.
Create:
User A
User B
Admin
Then try the application from each perspective.
Can User A access User B's resources?
Can User B call an admin endpoint?
Can an unauthenticated user access something that should require login?
Can a normal user change another user's object ID?
Can a deleted session still be used?
These tests often reveal things that a simple vulnerability scan won't.
"No Vulnerabilities Found" Is Not the Same as "Secure"
This is probably the most important distinction in this whole article.
A vulnerability scanner is extremely useful.
A WAF is extremely useful.
Dependency scanning is useful.
Code scanning is useful.
But every tool sees a different part of the system.
A scanner might tell you that your framework isn't running a known vulnerable version.
Great.
It doesn't necessarily know that:
User A → /api/invoices/1002 → User B's invoice
should have been blocked.
A WAF might block thousands of SQL injection attempts.
Great.
It doesn't necessarily know that a normal user shouldn't be able to approve a $50,000 transaction.
Your dependency scanner might report zero critical CVEs.
Great.
It doesn't know that your production server is exposing .git.
Security isn't one checkbox.
It's the combination of what your software contains, what your infrastructure exposes, and what your application allows people to do.
That's why I prefer thinking in terms of attack paths instead of vulnerability counts.
Ask:
> "If someone gets a normal user account, what can they reach?"
Then:
> "If they get an admin account, what can they reach?"
Then:
> "If one secret leaks, what does that secret give them?"
Those questions are much closer to how a real compromise happens.
What I'd Do Before Calling a Website Secure
Start with the basics.
First, map what is actually exposed.
Find your public domains, subdomains, APIs, admin panels, staging environments, cloud storage, and old infrastructure.
Then check the application itself.
Test authorization with multiple accounts. Test password reset. Test session expiration. Test file uploads. Test API endpoints.
Then check configuration.
Look for exposed .git, .env, backups, debug endpoints, directory listings, default credentials, and forgotten staging environments.
Then check known vulnerabilities.
Scan your CMS, plugins, frameworks, dependencies, and server software.
Then add runtime protection.
A WAF, rate limiting, logging, monitoring, and alerting can reduce the impact of attacks and help you see what's happening.
And keep checking.
Because your website isn't static.
You deploy new code.
Someone installs a plugin.
A developer creates a new API endpoint.
A staging server gets forgotten.
A cloud permission changes.
A secret gets committed.
Security changes with the application.
That's why a website can have zero known vulnerabilities today and still be one bad configuration away from a serious incident.
If you want to see what an attacker can discover from the outside, run a free security scan and start with the exposed surface, known weaknesses, and obvious configuration problems. You can then build the deeper checks around what the scan finds.
Don't stop when the scanner says "nothing critical."
That's when the more interesting questions start.
FAQ
Can a website really be hacked without a vulnerability?
Yes. Attackers can abuse exposed credentials, weak permissions, security misconfigurations, stolen sessions, or business logic flaws without exploiting a traditional software CVE. A system can be fully patched and still allow an attacker to do something they shouldn't.
What is the difference between a vulnerability and a misconfiguration?
A vulnerability is usually a weakness in software or application behavior that can be exploited. A misconfiguration is often a security setting or deployment mistake, such as exposing .git, enabling debug mode, or giving excessive cloud permissions.
Does a security scanner prove that my website is secure?
No. A scanner can find many important problems, especially known vulnerabilities and exposed weaknesses, but it can't prove that every business rule and authorization decision is correct. Security testing should combine scanning with authentication, authorization, configuration, and real application-flow testing.