WebSec. CTFs. Research.

0day speedrun? OpenFlagr <= 1.1.18 Authentication Bypass

Have you ever wondered what the fastest time is to discover a critical vulnerability?

Well, me neither, but here’s mine: 6 minutes.

Here’s another (as usual) short and interestingly uninteresting blogpost of mine about a 0day vulnerability I ~discovered~ speedrun inside of OpenFlagr <= 1.1.18 and the story behind it. I hope you enjoy it.

Side note: Due to issues with my previous disclosures, this disclosure and every following disclosure follows Google Project Zero’s 90+30 Vulnerability Disclosure Policy - in this case I’m disclosing 60+ days after the patch as agreed with project maintainers (no CVE yet cuz Mitre slow :/ ).

Intro:

A couple months ago I teamed up with the amazing team at Zenith Security to conduct a Web2 audit of an intensively huge codebase (50k+ LoC).

The audit was going amazing - great collabs, a lot of vulnerabilities & a fast turnaround with the patches (props to the team behind the code, if you happen to be reading this!).

However, after a certain time my progress plateau’d so, to refresh myself a bit, I started moving my focus to other areas such as the external attack surface, server infrastructure and etc… until I happened to come across a weird subdomain at xyz-flagr.redacted.tld .

The subdomain was only greeting us with a Basic Auth prompt like the following:

Screenshot 2026-01-03 at 4 28 30 AM

This kind of implementation is very easy, so it should be done well, right? Well, something nudged me - the codebase used a certain Flagr client library for Feature Flags, basically managing what code paths are enabled/disabled remotely.

When put into context getting access to this would be very bad as we could access anything hidden internally & expose sensitive data.

Sounds like a challenge to me!

The Speedrun:

At this point it was already ~6:45 AM for me.

The problem: I haven’t slept.

As you can see, my brain chooses to focus at very wrong times and I still haven’t found the solution to this. May fixing this be a New Year’s resolution of mine. Nights are very quiet and bring out a different kind of peace to me, so not sure how doable that is for me … but hey, where there’s a will, there’s a way 🙏

To not ruin my sleep schedule over curiosity (you can say this happened many times for me to even think like that) & auditing code I’m not paid to look at, I told myself: “You have 15 minutes to check this quickly and sleep bud”.

If you want to partake in a little challenge/practice, you can take the info you have at the moment and look at the patch diff, turn on the timer and see how long it takes you to find the vulnerability! Note: The time taken to setup your local instance shouldn’t be taken into account, as I already had an instance from the client - let me know your time in the comments :)

Opening the Flagr:

Per the Github Repository, this is what OpenFlagr is for:

Flagr is an open source Go service that delivers the right experience to the right entity and monitors the impact. It provides feature flags, experimentation (A/B testing), and dynamic configuration. It has clear swagger REST APIs for flags management and flag evaluation.

My only focus was bypassing the Basic Auth, as I knew anything behind it was juicy.

From what I noticed right away is that the main authentication OpenFlagr provides is via Basic/JWT authentication middleware. Here’s the implementation in pkg/config/middleware.go:

func (a *basicAuth) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
 if a.whitelist(req) { 
  next(w, req) // [1]
  return
 }

 username, password, ok := req.BasicAuth()
 if !ok || subtle.ConstantTimeCompare(a.Username, []byte(username)) != 1 || subtle.ConstantTimeCompare(a.Password, []byte(password)) != 1 {
  w.Header().Set("WWW-Authenticate", `Basic realm="you shall not pass"`)
  http.Error(w, "Not authorized", http.StatusUnauthorized)
  return
 }

 next(w, req)
}

The whitelist() method at [1] allows completely bypassing any form of authentication checks and directly hitting whitelisted API routes.

This is natural, as we want to allow certain unauthenticated behavior within OpenFlagr (such as /health routes and etc), however the implementation is quite interesting:

func (a *basicAuth) whitelist(req *http.Request) bool {
 path := req.URL.Path

 for _, p := range a.ExactWhitelistPaths {
  if p == path {
   return true
  }
 }

 for _, p := range a.PrefixWhitelistPaths {
  if p != "" && strings.HasPrefix(path, p) { // [2]
   return true
  }
 }
 return false
}

In [2] we can notice that the path we provide is being compared to whitelisted API routes via strings.HasPrefix(). The path has no sanitization/normalization whatsoever.

Here’s what PrefixWhitelistPaths looks like in pkg/config/env.go:

BasicAuthPrefixWhitelistPaths []string `env:"FLAGR_BASIC_AUTH_WHITELIST_PATHS" envDefault:"/api/v1/health,/api/v1/flags,/api/v1/evaluation" envSeparator:","`

Under these conditions, anything that starts with these endpoints will bypass the middleware as long as the backend handler accepts it, including paths like:

/api/v1/health/../foo/bar

This means that we might have an easy Authentication bypass via a Path Traversal.

Looking at the OpenFlagr API docs, the /export/sqlite route allows dumping the database.

I quickly send the request to the remote instance and to my surprise… it actually worked LOL:

Screenshot 2026-01-03 at 5 04 15 AM

Note: This should also affect the JWT middleware, as it uses the same implementation:

func (a *jwtAuth) whitelist(req *http.Request) bool {
 path := req.URL.Path

 // If we set to 401 unauthorized, let the client handles the 401 itself
 if Config.JWTAuthNoTokenStatusCode == http.StatusUnauthorized {
  for _, p := range a.ExactWhitelistPaths {
   if p == path {
    return true
   }
  }
 }

 for _, p := range a.PrefixWhitelistPaths {
  if p != "" && strings.HasPrefix(path, p) {
   return true
  }
 }
 return false
}

func (a *jwtAuth) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
 if a.whitelist(req) {
  next(w, req)
  return
 }
 a.JWTMiddleware.HandlerWithNext(w, req, next)
}

I look at the clock and notice it’s ~6:51 AM - 6 minutes. My personal record. Nice.

GIF

Outro:

Looking at more documentation (there’s many other API routes we can call/affect) afterwards it seems we ended up with a nice 0-click Authentication Bypass which allows full CRUD operations over flags, constraints, segments, exporting the database & etc.

Hope you enjoyed reading :)