OWASP Top 10 on my own 20-year-old website
I audited my own legacy websites against the OWASP Top 10:2025 and found nine of the ten categories. What I found, and how I fixed it.
Security architects spend their days reviewing other people's systems. In autumn 2026 I finally did what I recommend to every customer: I audited my own. My personal website started in 1999 and grew for two decades: an XML-based ASP.NET portal, PHP experiments, a few WordPress installations, an old .NET CMS and many small projects that I had simply forgotten.
The result was humbling and very educational. Nine of the ten categories of the OWASP Top 10:2025 (opens in a new tab) were represented. Here is what I found, mapped to the OWASP categories, and what I did about it.
The lesson in one sentence: the biggest risk in a website is not the code you maintain, it's the code you forgot about.
How I did it
- Make a copy first. I downloaded the whole server over FTPS into a local Git repository. That gave me a searchable copy and an undo button. The repository is local only, because configuration files contain credentials.
- Search, don't read. With tens of thousands of files you can't read everything. With the help of an AI coding assistant I searched for patterns:
phpinfo(,eval(base64_decode, hard-coded passwords,.sql,.bakand.zipfiles, and installer folders. The assistant did the searching; every decision and every change on the server was mine. - Check what is actually public. A risky file on disk is one thing; a risky file that answers 200 OK on the internet is another. I tested every finding against the live site.
- Quarantine, don't delete. Everything I removed was first moved to a folder outside the web root, tested, and only deleted later.
What I found
| Finding | OWASP Top 10:2025 | Fix |
|---|---|---|
| A 2007 PHP dictionary with SQL injection, and an update page that let anyone change entries without logging in | A05 Injection, A01 Broken Access Control | Rewritten: prepared statements, escaped output, read-only |
Several public phpinfo() pages showing the PHP version, paths and configuration | A02 Security Misconfiguration | Removed from the web root |
| WordPress installations from 2019–2020, an old .NET CMS, an abandoned web OS and an old rich-text editor | A03 Software Supply Chain Failures | Moved out of the web root |
| An HTTPS redirect that, because of a wrong condition, only applied to one search engine bot | A04 Cryptographic Failures | Redirect for everyone, plus HSTS |
| Portal passwords stored in plain text, and a public statistics file listing the user names | A07 Authentication Failures | Data folders blocked, passwords changed, portal retired |
| A "remember me" cookie containing the user's password in reversible encrypted form | A04 / A07 | Retired together with the portal |
| Database credentials hard-coded in application code | A07 Authentication Failures | Moved to a separate configuration file that is never served |
| An anti-spam script from 2013 that my antivirus flagged as obfuscated malicious PHP | A08 Software or Data Integrity Failures | Removed |
| A public log file with visitor IP addresses | A09 Security Logging and Alerting Failures (and GDPR) | Removed from the web root |
| Detailed ASP.NET error pages with stack traces, plus debug compilation and tracing enabled | A10 Mishandling of Exceptional Conditions, A02 | Generic error pages, debug="false", tracing off |
| Data from external APIs (game server names, chat user names) printed into HTML without escaping | A05 Injection (XSS) | Output encoding with htmlspecialchars |
The only category I did not find was A06 Insecure Design – although storing passwords in plain text was arguably a design decision too.
Five things I learned
1. Your attack surface is everything that answers on port 443
None of the worst findings were in the pages I visit. They were in folders I had not opened for ten years. Inventory comes before everything else.
2. Configuration is platform-specific
Several old projects came with .htaccess files written for Apache. My hosting runs IIS, which ignores them completely. Whatever rules you think are in place, verify them on the platform where they actually run.
3. Test the control, not the configuration
The HTTPS redirect existed in the configuration, so it looked fine. Only testing it as a normal visitor showed that it never triggered. The same applies to firewalls, MFA policies and backups: test the outcome.
4. Plain-text passwords are never "just a test site"
Short passwords stored in plain text, plus a public list of user names, turn a forgotten staging site into an entry point. Hash passwords with a modern algorithm (Argon2id, scrypt or bcrypt), or better, don't store them at all.
5. The most secure code is the code you delete
I replaced the old portal with a static website: no database, no login, no server-side code, no cookies and a strict Content Security Policy. I edit it through a small private editor, protected by a long password and a one-time code, that publishes plain HTML. There is almost nothing left to attack on the public site, and it is faster and cheaper to run.
A checklist for your own site
- Download a full copy and put it under version control (privately).
- Search for
phpinfo, installers, backups, dumps andeval(base64_decode(. - List every application and its version; remove what nobody uses.
- Turn off detailed errors and debug mode in production.
- Force HTTPS for everyone and add security headers (HSTS, CSP, X-Content-Type-Options).
- Check that no data, log or configuration folder is served to the internet.
- Move credentials out of the code, and change every password that was ever stored in plain text.
It took me a weekend to find all of this. An attacker's scanner needs a few minutes.