Blog / CVE explainers

CVE-2026-21589: Atlassian’s file read that hides behind ::

CVE-2026-21589 is an unauthenticated file read in eight Atlassian Data Center products. Here is the ordering bug behind it, and how to check your own code.

Completed doctoral studies in Information SecurityUniversity defense passed · Final dissertation defense pending

One library, eight products, one request from your config files

On 5 October 2026 Atlassian shipped an out-of-band fix for CVE-2026-21589, an unauthenticated arbitrary file read rated CVSS 4.0 9.3. [1] One flaw reaches eight Data Center and Server products: Jira Software, Jira Service Management, Confluence, Bitbucket, Bamboo, Crowd, Crucible and Fisheye. They don’t all break by coincidence. They share a library, atlassian-plugins-webresource, and the bug lives inside it. The advisory lists the fixed build per product; the library itself is fixed in 6.0.8. [1][2]

Atlassian is careful about the limits. An attacker can read “specific files within the web application root directory”, and only if they already know the exact path, with no directory listing. [1] That sounds narrow. It isn’t, and why it isn’t is the whole point of this post. I haven’t reproduced this one against a live instance; what follows is built from Atlassian’s advisory and watchTowr’s root-cause write-up, read on 8 October 2026. [1][2]

// atlassian-plugins-webresource, before 6.0.8
escapeSlashes(String string)   { return string.replaceAll("/", "::"); }
unescapeSlashes(String string) { return string.replaceAll("::", "/"); }

// How a web-resource name is handled on the way in:
resourceName                       // attacker-controlled
  -> check for ".." / "/" traversal   // sees "..::", no slash -> looks safe
  -> unescapeSlashes(resourceName)    // "..::"  becomes  "../"
  -> load file relative to web root   // traversal now active

The server believed :: was safe. The attacker controlled what it became

Here is the design the developers trusted. Web-resource URLs carry a resource key and a resource name. A forward slash inside that name would confuse the router, so the library escapes it: every “/” becomes “::” on the way in, and every “::” turns back into “/” later. [2] Encoding like that is reasonable. The bug is not the encoding. The bug is the order.

The code checks the incoming string for traversal first, while the slashes are still written as “::”. To that check, a sequence like “..::” contains no slash, so it reads like an ordinary file name and passes. Only afterwards does the library unescape the string, and “..::” becomes “../”. [2] The component that judged the request safe and the component that walked the filesystem were looking at two different strings. The attacker owns the gap between them.

Why a file read is really a takeover of your identity layer

Walk it through. The escaped, traversal-looking name clears validation, gets decoded back into real “../” segments, and is handed to the loader that reads from the classpath and the web application root. [2] watchTowr reports the read stays inside the Tomcat context, so you can’t climb out to /etc/passwd. For an attacker that boundary barely matters, because the files worth reading are already inside it.

The one to care about is WEB-INF/classes/crowd.properties. On products wired to Atlassian Crowd, that file holds the application name and the application password used to talk to the identity server. [2] An unauthenticated “read a file” bug now hands over a credential to the system that manages your users. watchTowr walks exactly that step: read the file, take the Crowd application password, and you are no longer reading files, you are speaking to the identity layer as a trusted application. [2]

That is the move the headlines skipped. “Reads specific files” and “hands an attacker your identity secrets” are the same bug, separated only by knowing which file to ask for. The advisory’s precondition, that you must know the exact path, is real, but crowd.properties is not a secret location. It is where the file always lives. [1][2]

# WEB-INF/classes/crowd.properties  (illustrative keys, not real values)
application.name       = my-confluence
application.password   = <shared secret to the Crowd identity server>
crowd.server.url       = https://id.example.internal/crowd/services/
crowd.base.url         = https://id.example.internal/crowd/

The patch reorders two steps. Make sure the reorder is what you deployed

The fix in atlassian-plugins-webresource 6.0.8 closes the gap by making the check and the filesystem see the same string: the name is decoded to its real form before the traversal check runs, not after. [2] The principle is worth keeping by name. Canonicalise first, then validate. A safety check only means something if it inspects the exact value that reaches the dangerous sink. The flow below is that corrected ordering as a review aid, not a copy of Atlassian’s diff.

For a product owner the trap here is believing the version number instead of the artifact. Several of these products ship more than one fixed branch, and a rollback image, a staging box or a forgotten Crucible server can still be loading library 6.0.7 while your notes say “patched”. [1] Check the jar that is actually loaded, on the instance that is actually reachable. A patched checkout on a developer’s laptop proves nothing about the box on the internet.

// The fix, as a principle (not Atlassian's exact diff):
// decode to the real value FIRST, then validate THAT value.
resourceName
  -> unescapeSlashes(resourceName)    // "..::"  becomes  "../"  up front
  -> check the decoded value for ".." traversal   // now the check sees it
  -> reject, or load file relative to web root

How I’d find this shape in code that isn’t Atlassian’s

This bug is not really about Atlassian. It is a class, and the class is “validate, then transform”. Any time code decodes, unescapes, normalises or canonicalises a value after it has checked that value for danger, you have the same hole, whether the sink is a file read, an SSRF or a SQL string. When I review code, the first thing I grep for is a decode step sitting downstream of a validation step on the same variable.

The pattern below is where I start. It surfaces the decode and sink calls; the real work is manual. For each hit I ask one question: is the string the “..” check looked at byte-for-byte the string that reaches the filesystem? If a transform sits in between, the check is inspecting a value that no longer exists by the time it matters. That question finds this bug faster than any scanner signature, because the scanner matches names and I’m matching order.

# Smell to hunt: a decode/normalise step that runs AFTER the path check,
# on the same variable. Start broad, then read each hit by hand.
rg -n "replaceAll|URLDecoder|decode|unescape|normalize|canonical" \
   --glob '!**/test/**'

# The one question that confirms the bug:
#   is the string that reaches the file sink byte-for-byte the string
#   the ".." check inspected? If a transform sits between them, it is vulnerable.

Is this your fire? For some of you, yes, today

Be honest about scale. This affects self-hosted Data Center and Server only. Atlassian says Cloud instances were patched and shows no evidence of exploitation there. [1] If you are entirely on Atlassian Cloud, this is not your incident. If you self-host any of the eight products and expose it to the internet, it is, and the clock already started.

watchTowr published the root cause and a proof of concept on 6 October and puts the exposed population in the “six to seven-figure range”, with just under 700,000 Confluence instances alone by their count. [2] Treat that number as theirs, not mine. What is not in dispute is the speed. The Hacker News and BleepingComputer report that honeypots saw exploitation attempts within about two hours of the details going public, so far mostly fingerprinting and sweeping for config files. [3][4] As of 8 October it is not in CISA’s Known Exploited Vulnerabilities catalog, which tells you it is early, not that it is quiet. [3]

So the three conditions for this to be your emergency are simple. You self-host one of the eight products. It is reachable from the internet, or from a network segment you don’t fully trust. And it ran an unpatched library build during the window since 5 October. If all three are true, patching is step one, not the whole job, because a file that was readable may already have been read.

What to do before you close this tab

The list below is the order I’d work in. Patch first, because the mitigation window here is measured in hours, not weeks. [3] But if you self-host and you were exposed, assume the Crowd application password and anything else under the web root could have been read, and rotate it. A patch stops the next read. It does not un-leak a secret that already left.

The access logs are where you find out whether anyone tried. Pre-patch requests to the web-resource routes carrying “::”, “%3a%3a” or “..” in the resource name are the signature to hunt for. A hit is not proof of a successful read, but it tells you which instances to treat as suspect and which secrets to rotate first.

Patch and verify (in this order):
  [ ] List every self-hosted install: Jira, JSM, Confluence, Bitbucket,
      Bamboo, Crowd, Crucible, Fisheye.
  [ ] Confirm the build that is RUNNING, not the one in your notes.
  [ ] Patch each product to its fixed version from the advisory [1].
  [ ] Cannot patch yet? Apply Atlassian's WAF / RewriteValve mitigation [1].

If you were exposed and unpatched, assume read, then rotate:
  [ ] The Crowd application password in crowd.properties.
  [ ] Any other secret stored under the web application root.

Hunt the access logs for pre-patch probing:
  grep -E "/(s|resources|sources)/[^ ]*(::|%3a%3a|\.\.)" access.log

The part a version bump won’t tell you

CVE-2026-21589 is a clean example of a bug you can’t see from the version number. The fix reorders two internal steps, and the only way to know your deployment is safe is to look at the artifact that is running and the code path that reaches the filesystem. That is the gap between “we applied the patch” and “we verified the thing we care about is closed”.

Tracing a request from the edge, through the framework, to the exact line that decides what gets read is the core of a secure code review, and the “validate then transform” smell in this CVE is one I look for in code that has nothing to do with Atlassian. If you are shipping something that turns outside input into file access, database queries or internal requests, that is the boundary I’d check for you. Tell me what you are running and we’ll scope it.

Sources

Need this checked in your own product?

Security code review
All articles

08Contact

Shipping something that handles user data?

Send me a short brief. I reply within three days with questions or a proposal outline, and the price is agreed in writing before work starts.