September 2, 2026
When Debug Tooling Ships to Production: How Keycloak’s Admin and Account UI Leaked Their Own Source…
Reported by Swornim Poudel
By k3tu
3 min read
Not every information-disclosure bug needs a clever exploit chain. Sometimes the leak is sitting right there, one file extension away from anything a browser already downloaded. This one came from Keycloak quietly shipping JavaScript source maps for its Admin UI and Account UI into production, no authentication required, no rate limiting, just append .map to a filename and read.
What Is Keycloak
Keycloak is an open-source Identity and Access Management (IAM) platform used to handle authentication, authorization, and user federation for countless applications. Its Admin UI and Account UI are modern React front ends, bundled and minified like any production JavaScript app before being served to the browser.
The Bug, in Plain Terms
Modern JS build tooling (webpack, vite, etc.) generates .js.map source map files alongside every bundled script. These maps exist to help developers debug minified code in browser devtools, mapping mangled production output back to original, readable source: real file names, real variable names, real function structure.
Keycloak's build pipeline generated these maps as normal, but nothing stopped the running server from serving them back to anyone who asked.
How to reproduce, straight from the report:
Pick any JS file the Admin UI or Account UI loads in the browser, and append .map to its filename.
That's it. No auth, no special tooling, just a URL.
Why This Matters
Keycloak is open source, so in principle its source is already public. But that's only true for the stock UI. Organizations frequently customize the Admin UI and Account UI, adding internal logic, custom branding, or organization-specific flows. Source maps for a customized build don't reconstruct Keycloak's public GitHub repo, they reconstruct whatever internal code was compiled into that specific deployment.
Handing that back to anyone who requests it defeats the entire point of minification as a mild obfuscation boundary, and gives an attacker a fully-annotated map of a target's custom frontend logic for free.
The Real-World Consequence
Because the server had no filter on .map requests, every production Keycloak instance running an affected version was, by default, handing out a blueprint of its own frontend internals to anyone who thought to ask. No error, no warning, no log entry calling it out, just a 200 response with the file.
This was flagged as a regression: earlier behavior did not expose these files the same way, something changed that let source maps start flowing to production responses.
The Fix
The patch (PR #48301) takes the simplest possible approach: don't try to stop the maps from being generated, just stop the server from ever handing them out once it's running in production.
Three pieces:
A dev-mode check:
public class IsKeycloakDevMode implements BooleanSupplier {
@Override
public boolean getAsBoolean() {
return Environment.isDevMode();
}
}public class IsKeycloakDevMode implements BooleanSupplier {
@Override
public boolean getAsBoolean() {
return Environment.isDevMode();
}
}A build step that only registers the filter outside dev mode:
@BuildStep(onlyIfNot = IsKeycloakDevMode.class)
void filterSourceMapRequests(BuildProducer<FilterBuildItem> filters) {
filters.produce(new FilterBuildItem(new RejectSourceMapFilter(), SecurityHandlerPriorities.CORS + 1));
}@BuildStep(onlyIfNot = IsKeycloakDevMode.class)
void filterSourceMapRequests(BuildProducer<FilterBuildItem> filters) {
filters.produce(new FilterBuildItem(new RejectSourceMapFilter(), SecurityHandlerPriorities.CORS + 1));
}And the filter itself, which simply 404s any request for a .js.map path:
public class RejectSourceMapFilter implements Handler<RoutingContext> {
private static final Logger LOGGER = Logger.getLogger(RejectSourceMapFilter.class);
@Override
public void handle(RoutingContext routingContext) {
String path = routingContext.normalizedPath();
if (path.endsWith(".js.map")) {
LOGGER.debugf("Blocked source map request: %s", path);
routingContext.fail(404);
return;
}
routingContext.next();
}
}public class RejectSourceMapFilter implements Handler<RoutingContext> {
private static final Logger LOGGER = Logger.getLogger(RejectSourceMapFilter.class);
@Override
public void handle(RoutingContext routingContext) {
String path = routingContext.normalizedPath();
if (path.endsWith(".js.map")) {
LOGGER.debugf("Blocked source map request: %s", path);
routingContext.fail(404);
return;
}
routingContext.next();
}
}Source maps are still generated and kept on disk for support/debugging purposes, and still served in dev mode where they're actually useful. In production, any request ending in .js.map now gets a clean 404 instead of a full readable source dump.
Affected Package and Versions
Package: org.keycloak (Admin UI / Account UI, Quarkus distribution) Affected versions: 26.5.x (regression from earlier behavior) Patched: merged via PR #48301 (May 5)
Who Was Affected
Any production Keycloak deployment serving the Admin UI or Account UI on an affected version was exposing its .js.map files by default. Risk was low for stock, uncustomized deployments since that source is already public on GitHub, but materially higher for any organization running a customized Admin/Account UI, where the maps would reveal internal-only frontend code that was never meant to be public.
The Bigger Lesson
- Build tooling defaults are not security decisions. Source maps exist for developer convenience; nobody explicitly decided they should be public in production, they just were, because nothing said otherwise.
- "Open source" doesn't mean "every deployment's code is meant to be public." A customized fork or theme is not the same artifact as the public repo, and treating it that way leaks real internal work.
- The absence of a control is itself a finding. This wasn't a broken check, it was a missing one, and those are often the easiest bugs to overlook because there's no faulty logic to spot, just logic that was never written.
- Silent exposure beats loud exploits for staying power. No crash, no anomaly, no alert; just a file sitting there, answering 200 to anyone who asked, for as long as nobody thought to ask.
Reported against the keycloak/keycloak project. Filed as a hardening issue rather than a formal CVE, per maintainer triage. Fixed in the Quarkus runtime via a production-mode filter blocking .js.map requests.
Reference:
GitHub Issue: https://github.com/keycloak/keycloak/issues/47545 GitHub Pull Request (fix): https://github.com/keycloak/keycloak/pull/48301