Skip to content

Outdated API versions remain reachable

CWE-1059CWE-285OWASP A05:2021Updated September 4, 20264 min read

When a new API version is released, the old one often keeps running for clients not yet migrated. That old version gets no further attention, misses the authorisation checks and validation added since, and thereby forms a detour around all those improvements.

When a team releases a new version of its API, attention goes to what is new. The old version keeps running because clients still depend on it, and thereby disappears from view, though not from the infrastructure. The trouble is that the abandoned version misses exactly the checks you have added since.

Why does an old API version stay online?

We speak of outdated API versions remaining reachable when an older generation of endpoints keeps running alongside the current one without receiving the same maintenance. It is not a design flaw but a consequence of how APIs evolve in practice: you cannot cut off existing clients overnight, so the old version runs alongside for a while. That “a while” becomes years.

What happens in the meantime is the actual problem. Every improvement to the new version, a stricter authorisation check, an input validation schema, a rate limit, fewer fields in the response, is applied there and not to the old one. The gap grows silently, and the old version thereby becomes a detour around all those improvements.

Compare it with a building where a new entrance with access control has been constructed, while the old door at the back stays open because a few people still hold an old key. All the attention and all the measures sit at the new door. The burglar goes to the old one.

How does an attacker find old API versions?

Vulnerable:

// The current version, with all the checks
app.get('/api/v3/customers/:id',
  requireLogin,
  requireRole('accountmanager'),
  rateLimit,
  async (req, res) => {
    const customer = await customers.findForManager(req.params.id, req.user.id);
    res.json(customerSummary(customer));            // limited fields
  },
);

// The old version, never touched again
app.get('/api/v1/customers/:id', requireLogin, async (req, res) => {
  const customer = await customers.find(req.params.id);
  res.json(customer);                                // the full record
});

Both routes exist, both on the same server. The current one checks the role, limits the rate, verifies the customer belongs to this account manager and returns only the intended fields. The old one asks for a valid session and does nothing further. An attacker with an ordinary account only has to change the version number:

GET /api/v3/customers/8891 HTTP/1.1     → 403 Forbidden
GET /api/v1/customers/8891 HTTP/1.1     → 200 OK, full record

Finding it takes little effort. Version numbers are predictable, old paths often still appear in archived documentation, and an outdated mobile application still in circulation reveals exactly which endpoints once existed.

Safe:

// Every version runs through the same authorisation and validation layer
const base = [requireLogin, rateLimit, checkAccess];

app.get('/api/v3/customers/:id', ...base, v3.showCustomer);
app.get('/api/v2/customers/:id', ...base, v2.showCustomer);

// And what is no longer supported is genuinely closed
app.use('/api/v1', (req, res) => {
  res.status(410).json({
    error: 'This API version is no longer supported',
    documentation: 'https://portal.example/api/migration',
  });
});

The shared layer ensures an improvement to authorisation applies immediately to every supported version. And a version you are retiring returns a 410 instead of quietly continuing to work, which is clear for the clients still on it and closes the detour.

It is not only about version numbers. Endpoints that were replaced but still exist, test routes that ended up in production, and temporary connections for a migration that was never cleaned up all present the same picture: functionality running without anyone still looking at it.

What is the impact of outdated API versions?

The severity runs from medium to high and is determined by how large the gap between versions has grown. With a difference of a few months it is modest; with an old version frozen years ago, all the checks added since may be missing.

What characterises this finding is that it undoes the value of your own improvements. A team that properly implemented object-level authorization, tightened input validation and reduced the number of returned fields did all of that in the new version. As long as the old one is reachable, effective security is that of the weakest version.

On top of that, old versions are rarely covered by monitoring. Alerts fire on unusual use of the current endpoints; the old ones often do not even appear in the overview. An attacker systematically extracting data there is therefore less likely to be noticed.

How do you detect outdated API versions?

For every endpoint found, a tester tries whether other versions exist: lowering the version number, trying other spellings, and searching for paths such as /api/old, /api/beta or /api/internal. For each endpoint found they then compare which checks are present and which are not.

Sources outside the application are used as well. Archived documentation, old versions of the mobile application, JavaScript bundles from earlier releases and search engine results regularly yield endpoints that are mentioned nowhere but still answer. Acceptance and test environments reachable from the internet get attention too, because they often run an older version with comparable data. AssistSec reconciles that inventory with your own overview, because the difference between what the documentation says is running and what actually answers is usually the heart of the finding.

How do you prevent outdated API versions?

  • Keep an up-to-date overview of all API versions and endpoints that are genuinely reachable.
  • Route every supported version through the same authorisation, validation and rate-limiting layer.
  • Announce the retirement of a version with an end date and enforce it.
  • Answer retired versions with a clear 410 instead of letting them run on quietly.
  • Use your log files to see who still uses an old version, and approach those users directly.
  • Remove test routes, temporary connections and replaced endpoints as soon as they are no longer needed.
  • Shield acceptance and test environments with network restrictions or authentication.
  • Include all versions in monitoring and alerting, not only the current one.
  • On every security improvement, check whether it also applies to the older versions still running.

Sources

Frequently asked questions

How do I know which versions are still running?

Through an inventory that does not lean on documentation but on reality: route configuration, log files from recent months, and actively probing predictable paths. Documentation describes what should exist; the logs tell you what is actually being called.

What if customers are still on the old version?

Announce an end date, approach the users of that version directly based on your logs, and then enforce it. While the old version keeps running, at least route it through the same authorisation and validation layer as the new one. Outdated does not have to mean unmaintained.

Are test environments the same problem?

In practice often worse. An acceptance environment has the same functionality, less attention and sometimes real data. If it is left reachable from the internet, it is a more attractive target than production itself.

Does removing the version from the documentation help?

Barely. Old endpoints are found by trying predictable paths, by examining old clients and by looking in archived documentation. Obscurity is not a measure; closing them off is.

Related articles

Press / to search · Esc