Benjamin Looi
Back to blogs
I Found a Security Nightmare Inside a Company's 10-Year-Old PHP App

I Found a Security Nightmare Inside a Company's 10-Year-Old PHP App

Benjamin Looi / May 5, 2026

Some friends told me their company was hiring. The pay sounded decent, the stack was steady, and I would get to see my friends every day at the office. I went through the interviews, felt good about them, and then came the salary conversation. Their number came in short of what I needed. We could not agree, and that should have ended the story.

It wasn't.

Because I am a software engineer, and software engineers poke around systems they are curious about. That is not a character flaw. We see a door and wonder what sits on the other side.

So I had a look. I found a relic.


The Application

The company runs an internal PHP application that has served thousands of employees for over ten years. Ten years is a long time in PHP. The app had been maintained loosely by a rotating cast of developers, with each person leaving a few risky patterns behind before moving on.

Here's what I found:

// findings.log

  PHP version: severely outdated (end-of-life, no security patches)
  phpMyAdmin: publicly exposed, no IP restriction, no 2FA
  Password hashing: MD5 / plain SHA1, circa 2005
  Directory listing: enabled on the public web root
  Hardcoded DB credentials and API keys in plain .php files
  Login form: no rate limiting, wide open to brute force
  SQL queries built by string concatenation, textbook injection bait
  User uploads stored in a public directory, no file type validation
  Single server, no redundancy, one crash takes everything down
  No staging environment, changes go directly to production
  Deployments via FTP, or direct file edits on the live server
  No version control. A shared network drive.
  Copy-pasted logic repeated across dozens of files
  Application is running. Technically.

This was not a side project on a developer's laptop. It was a live production system that thousands of people used every workday. Their credentials and internal records sat behind security practices the industry abandoned years ago.


A Closer Look at the Horrors

Credentials in Plain Sight

Somewhere in that codebase, a .php file holds the database username and password as plain text strings. It may also hold an API key for a third-party service. These values are often at the top of a file, copied from developer to developer over the years. Anyone with read access and bad intentions could have taken the database.

The Login Form Was an Open Invitation

The login form had no rate limiting. You could try ten thousand password combinations and the server would process every one. Combined with outdated hashing, a leaked credential dump would be easy to crack. Credential stuffing attacks are automated and indiscriminate. They do not need to know your company exists to find you.

SQL Injection, the Classic

The queries were built by stitching user input directly into SQL strings. Something like:

$query = "SELECT * FROM users WHERE username = '" . $_POST['username'] . "'";

Security tutorials use this kind of code as the example of what not to do. It was everywhere. A single crafted input could have exposed, modified, or deleted the database. Developers have documented, warned about, and taught against this vulnerability since the late 1990s.

Files Uploaded. Validated? Never.

Users could upload files. Those files landed in a public directory. The app did not check file type, inspect content, or sanitize filenames. In practice, a user could upload a .php file and execute it if the server allowed it. That attack vector has been documented for years. The fix is small. The oversight had been there for years.

One Server. No Net.

The entire application ran on a single server. No load balancer, no failover, no redundancy. A hardware fault, botched update, or full disk could lock every employee out. Because deployments happened through FTP or live edits on the production server, a developer pushing a change at the wrong moment could become the outage. Nobody had built a safety net.


The Code Itself

Beyond the security issues, the codebase showed a decade of quiet neglect. Developers had copied and pasted logic across dozens of files. Each copy drifted as people made small changes over the years. When a bug existed in that shared logic, it existed in twenty places, and fixing it meant hunting each one down manually.

There was no version control. Not an outdated Git setup, not an old SVN repository. Nothing. Changes were tracked, if at all, by whoever happened to remember making them. The codebase had no history. There was no way to answer the question "what changed last Tuesday?" because nothing had ever recorded the answer.

The worst part was not finding the problems. The worst part was realizing that the team had learned to treat them as normal.


The Part Where I Told Someone

I wrote up my findings and brought them to the CEO. I chose the CEO instead of the IT team because the IT team was part of the story. The CEO listened, took it seriously, and asked me to prepare a formal proposal.

I outlined a phased modernization plan: move everything into version control, containerize the environment, upgrade PHP incrementally, migrate password hashing to bcrypt, lock down phpMyAdmin, add a staging environment, and start a basic security review cycle. Nothing revolutionary. Just the fundamentals.

The CEO forwarded my proposal to the IT team.

The IT team read it and confirmed they could implement everything in it.

Then the story turned.


The Proposal Becomes a Negotiation Tactic

Because the IT team said they could do all of it, the company felt no urgency to bring in someone new at a higher package. My own proposal became evidence that they did not need me.

The thinking, as best I could reconstruct it: we already have people who can fix this, so we do not need to pay a premium for someone who identified that it needed fixing.

The issues I documented had existed for years. The team capable of fixing them had been there the whole time. The problem was not ability. It was awareness, prioritization, and a culture that had stopped questioning the system.

I left without an offer. I do not know whether they implemented any of the fixes.


What This Story Is Really About

Legacy systems do not rot in one dramatic moment. They decay over years as people inherit code they did not build and learn to work around it. Every hack, workaround, and "we'll fix this later" becomes part of the structure. The technical debt compounds until the codebase stops feeling like a product and starts feeling like something nobody wants to touch.

The exposed phpMyAdmin, SQL injection, and hardcoded credentials were serious. The deeper danger was the knowledge gap: the distance between current industry practice and what the team maintaining the system believed was normal. When you stare at the same system long enough, you stop noticing what should feel wrong.

That is an organizational failure, not only a technical one.

Tech debt does not show up as a line item. Leadership looks at a system that is "running fine" and sees no cost because the cost has not arrived yet. It arrives gradually as the system becomes harder and more expensive to change, or suddenly as a breach, outage, or compliance failure.


What Should Have Happened (And Still Should)

Version control should have been non-negotiable on day one. Without it, you cannot know what changed, when, or who changed it. Every change to that codebase for ten years happened in the dark.

A staging environment is not a luxury. It is the minimum bar for responsible deployment. "We push to production and see what happens" is gambling with a live system.

Credentials belong in environment variables, not source files. This is not advanced knowledge. It is covered in the first chapter of every modern web framework's documentation.

Parameterised queries have been the standard for SQL since the early 2000s. There is no longer any excuse, in any language, for concatenating user input into a database query.

Rate limiting a login form is a single middleware configuration. It takes minutes. It closes the door on an entire category of attack.

None of this is exotic knowledge. It is first-year web security curriculum. Finding it in a production system used by thousands of people made the situation unsettling.


The organization I visited is not unusual. That is the point. Variations of this system exist inside thousands of companies right now, serving employees while accumulating risk.

If you're a developer reading this: run the honest audit on your own systems. Not the comfortable one. Check the PHP version. Check how you're hashing credentials. Ask when the dependencies were last updated. Ask if there's a .git folder anywhere in the project. Ask what happens if the server goes down tonight.

The most expensive systems to fix are the ones where nobody asked those questions early enough.


Thanks for reading! 😁

Comments