I finally landed my first CVE. No fancy memory-corruption 0-day, no clever exploit chain. Just a plain old path traversal in Frappe LMS that taught me more about bug hunting than any course I’ve taken.

This is the story of CVE-2026-54343: how I picked the target, how I found the bug, and why a “boring” file-read issue ended up rated Critical. I’m keeping it high-level on purpose. Enough to understand what happened, not a copy-paste recipe for pointing it at a live server.

The bug is already fixed. If you run Frappe LMS, update to 2.52.0 or later and you’re good. Read the official advisory for the authoritative details.

TL;DR

Product Frappe LMS
CVE CVE-2026-54343 (GHSA-3mq2-3c8v-m92j)
Class Path Traversal / Arbitrary File Read (CWE-22)
Affected <= 2.51.0
Patched 2.52.0
Severity Critical

In short: the part of the app that serves SCORM e-learning content built a file path out of the request URL without checking that the result stayed inside the folder it was supposed to. That let a request reach files it had no business reaching, including config files that hold sensitive secrets. No login required.

1. Picking the target

I didn’t have a target in mind, but I did have criteria. Everyone gives the same advice: stop chasing the hardest thing on the internet and start reading code that real people actually run. So I set my filters first. Open-source, written in Python so I could read it fluently, and somewhere in the 1,000 to 5,000 GitHub star range.

That star range is deliberate. Under a thousand and a project is usually too small to matter or too rough to trust. Up in the tens of thousands and every researcher and their dog has already combed through it. The 1k to 5k band is the sweet spot: popular enough that a bug actually matters, obscure enough that nobody’s picked it clean yet.

From there it was just reading. I lined up a few projects that fit, opened each one, and looked at what they actually exposed: how big the surface was, how deep user input travelled, how many features looked like they handled complex input. Frappe is where I stopped, and I picked it for two reasons:

  • Huge attack surface. It’s a full framework, not a tiny library. Massive feature set, tons of endpoints, lots of moving parts. More surface, more places for something to slip through.
  • Interesting functions. Reading through it, I kept hitting features that take complex user input and do something heavy with it. File uploads, archive extraction, serving content back to users. That’s where bugs tend to live, and knowing which ones are worth chasing is a feel you build by reading a lot of code.

2. Following the feature that smelled risky

The feature that caught my eye was SCORM support. SCORM packages are uploaded archives full of HTML and other content that the platform unpacks and later serves back to learners.

Any time an app accepts a file, unpacks it, and serves files out of it, I slow down and read carefully. A lot can go wrong in that flow, and the part that serves the content back out is the part people forget to check.

So instead of poking at the running app from the outside, I went straight to the code that serves SCORM files and read it carefully. I had a theory going in: this builds a file path straight from the request, so is anything actually stopping it from wandering off? I followed how the request flowed through the function until I could confirm what I was seeing. That’s where it turned up.

3. The bug, in plain terms

The component that serves SCORM content takes the path from the incoming request and uses it to build a location on disk. The problem: it trusted that path completely. Nothing checked that the final location actually stayed inside the SCORM folder.

That’s the whole bug. Build a filesystem path out of user input without confirming it’s contained, and the request can walk out of the intended folder and land somewhere else on the server. The classic name for this is path traversal (CWE-22), and it’s been on the OWASP radar for as long as I’ve been alive.

What makes it bad here is what lives nearby on a Frappe server: config files that store secrets in plaintext. A feature meant to serve harmless course content could be steered toward files that were never meant to be exposed, and you don’t even need to log in.

I’m not publishing the exact requests. The shape of the bug is enough to understand it. The working payloads stay in the private report, not on a public blog.

4. Why a file-read bug got rated Critical

When I first confirmed it, part of my brain went “it’s just reading a file, is that even a big deal?” Turns out, yes. That’s the lesson that stuck with me.

A read-only bug sounds low-impact until you ask what it can read. On a Frappe box, an unauthenticated attacker reaching the wrong config file gets the secrets that protect everything else. From there it grows fast, and on servers hosting multiple sites side by side, the damage isn’t even limited to one tenant.

That’s why the maintainers rated it Critical instead of something middling. A plain info leak can quietly be a skeleton key. Now, whenever I find a file-read primitive, my first question is: what’s the most sensitive thing this could reach?

5. The fix

The fix is the textbook one, and it’s worth burning into memory because it applies to every file-serving route you’ll ever review:

Before opening anything, resolve the requested path and confirm it still sits inside the intended directory. If it escapes, refuse.

The advisory sums up the patch as: “File paths are now checked to stay within the SCORM directory before serving.” That one containment check is the whole difference between a safe feature and a Critical CVE. It shipped in v2.52.0.

6. Disclosure

I reported it privately through Frappe’s GitHub Security Advisory process. The team triaged it, confirmed it, shipped the fix in 2.52.0, and the advisory went out with CVE-2026-54343 assigned. The whole thing was smooth and professional, so credit to the Frappe team for taking it seriously and turning it around fast.

7. What I’m taking away from my first CVE

  • You don’t need a hard target. Popular, open-source, readable language, big feature set. That’s enough to start.
  • Read the parts everyone skips. The bug wasn’t visible from the outside. It sat in the code that serves files, where most people never look.
  • Follow the upload. Features that accept files, unpack them, and serve them back are gold. The unpacking and the serving both deserve attention.
  • “Just a file read” can be Critical. Always ask what the most sensitive thing the bug can reach actually is.
  • Containment, not blocklists. The fix for traversal isn’t filtering scary characters. It’s resolving the path and proving it stays inside the allowed folder.

This was the first of a few things I dug out of Frappe during the review, but it’s the one that earned the CVE, and the one that made me believe I could actually do this. If you’re chasing your own first CVE: pick something real, read the code others ignore, and follow every piece of user input until it touches a file, a database, or the page.

See you on the next one. ッ