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

  1. 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.
  2. 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, .bak and .zip files, and installer folders. The assistant did the searching; every decision and every change on the server was mine.
  3. 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.
  4. 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

FindingOWASP Top 10:2025Fix
A 2007 PHP dictionary with SQL injection, and an update page that let anyone change entries without logging inA05 Injection, A01 Broken Access ControlRewritten: prepared statements, escaped output, read-only
Several public phpinfo() pages showing the PHP version, paths and configurationA02 Security MisconfigurationRemoved from the web root
WordPress installations from 2019–2020, an old .NET CMS, an abandoned web OS and an old rich-text editorA03 Software Supply Chain FailuresMoved out of the web root
An HTTPS redirect that, because of a wrong condition, only applied to one search engine botA04 Cryptographic FailuresRedirect for everyone, plus HSTS
Portal passwords stored in plain text, and a public statistics file listing the user namesA07 Authentication FailuresData folders blocked, passwords changed, portal retired
A "remember me" cookie containing the user's password in reversible encrypted formA04 / A07Retired together with the portal
Database credentials hard-coded in application codeA07 Authentication FailuresMoved to a separate configuration file that is never served
An anti-spam script from 2013 that my antivirus flagged as obfuscated malicious PHPA08 Software or Data Integrity FailuresRemoved
A public log file with visitor IP addressesA09 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 enabledA10 Mishandling of Exceptional Conditions, A02Generic error pages, debug="false", tracing off
Data from external APIs (game server names, chat user names) printed into HTML without escapingA05 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 and eval(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.