Thank you for helping keep Kora-Frontend secure. This document explains how to report security issues and our approach to handling them.
- Preferred: Open a private GitHub security advisory or email the maintainers at security@openledger.foundation (replace with the real address).
- If sensitive data is involved, do not create a public issue or PR.
Provide:
- Affected components and versions.
- Steps to reproduce (minimal repro preferred).
- Impact assessment and suggested mitigation.
We support the main branch and the latest released tag. If you're unsure whether a version is supported, contact us.
- Acknowledge receipt within 3 business days.
- Triage and reproduce the issue.
- Coordinate fix and disclosure timeline with the reporter.
- Release a fix and public advisory if appropriate.
We welcome good-faith security research. Please avoid privacy violations, data destruction, and denial-of-service testing without prior consent.
POST /api/auth/challenge issues a one-time nonce that the wallet signs to prove key ownership. The nonce is stored in an in-memory Map inside app/api/auth/challenge/store.ts.
| Property | Value | Rationale |
|---|---|---|
NONCE_TTL_MS |
10 minutes | Long enough for a user to read and sign; short enough to limit the replay window. |
MAX_NONCES |
500 | Prevents unbounded heap growth under a serverless flood. |
| Eviction strategy | TTL first, then oldest-first | Stale entries are always removed before the size cap is enforced. |
TTL eviction — evictStaleNonces() is called automatically at the start of every generateNonceChallenge() invocation. No background timer is needed.
Size cap — After TTL eviction, if the store still holds ≥ MAX_NONCES entries, the oldest nonces (insertion order) are removed until the count is below the cap. Under a flood, older challenges are invalidated rather than new ones being rejected.
Single-use — consumeNonce(nonce) marks a nonce as used: true on the first successful call. Any subsequent call with the same nonce returns false, preventing replay attacks.
Expired consumption — If a nonce passes its TTL between generation and consumption, consumeNonce returns false and removes the entry.
Date.now() is used on the server. Sub-second drift between edge instances is acceptable — a nonce that expires a few milliseconds early is safely regenerated. In a multi-instance deployment, nonces are not shared across instances; clients must verify against the same instance that issued the challenge, or a Redis-backed store variant must be used (see docs/architecture.md).
| File | Role |
|---|---|
app/api/auth/challenge/store.ts |
In-memory nonce map, TTL, size cap, single-use logic |
app/api/auth/challenge/route.ts |
POST handler — CSRF-guarded, calls generateNonceChallenge() |
app/api/auth/challenge/__tests__/store.test.ts |
Unit tests: TTL, single-use, flood, eviction |
app/api/auth/challenge/__tests__/route.test.ts |
Route-level integration tests |
All mutating API routes (POST /api/auth/challenge, POST /api/auth/verify, POST /api/feedback) are protected against Cross-Site Request Forgery using the double-submit cookie pattern.
-
Token issuance — On page load (or session change), the client calls
GET /api/auth/csrf. The server:- Generates a cryptographically random token via
crypto.randomUUID(). - Sets it as a
__kora_csrfcookie withHttpOnly; SameSite=Strict; Secureflags. - Returns the same token in the JSON body.
- Generates a cryptographically random token via
-
Token attachment — The client reads the token from the response body and attaches it as the
x-kora-csrfrequest header on every mutating request. -
Token verification —
verifyCsrf(request)inlib/csrf.tsreads both the__kora_csrfcookie and thex-kora-csrfheader. If either is absent or they do not match, the route returns403 Forbidden.
Cross-origin requests cannot read HttpOnly cookies, so an attacker cannot forge the correct header value even though the browser automatically sends the cookie. The header can only be set by JavaScript running on the same origin.
Calling GET /api/auth/csrf again issues a new token and overwrites the previous cookie, invalidating prior tokens. Rotation should happen on authentication state changes (sign-in / sign-out).
| File | Role |
|---|---|
app/api/auth/csrf/route.ts |
Issues the token and sets the cookie |
lib/csrf.ts |
verifyCsrf() and issueCsrfToken() helpers |
app/api/auth/challenge/route.ts |
Protected — calls verifyCsrf() |
app/api/auth/verify/route.ts |
Protected — calls verifyCsrf() |
app/api/feedback/route.ts |
Protected — calls verifyCsrf() |
- Never commit secrets or credentials. Use environment variables and secret stores.
- Keep dependencies up to date; run
npm auditand address high/critical issues. - Validate and sanitize all inputs, especially when interacting with contract builders and IPFS uploads.
- Call
GET /api/auth/csrfbefore any form submission that targets a protected route and attach the returned token as thex-kora-csrfheader.
We run automated dependency vulnerability scanning in CI using npm audit --audit-level=high. The build will fail if any unexcepted high or critical severity vulnerabilities are detected.
If a vulnerability is identified as a false positive, does not affect Kora-Frontend due to our specific usage, or is a development-only threat, it can be bypassed by adding an entry to audit-exceptions.json.
Each exception must include:
advisoryId: The security advisory ID.package: The name of the package.url: The link to the GitHub Advisory or vulnerability report.reason: A detailed justification explaining why this vulnerability is not exploitable or applicable in our project.
The CI environment executes scripts/audit.js to enforce these checks.
Create a private GitHub Security Advisory or email security@openledger.foundation.
Content Security Policy (CSP) violations are automatically reported to /api/csp-report, which logs sanitized violation details including:
document-uri(stripped of query params to remove PII)violated-directive(e.g.,script-src,img-src)blocked-uri(the resource that was blocked)source-file,line-number,column-number(when available)
Production deployments should:
- Monitor CSP report logs — violations indicate nonce regressions, third-party script breakages, or potential XSS attempts.
- Rate limit enforcement — the endpoint limits reports to 10 per IP per 5 minutes to prevent abuse.
- PII sanitization — all
document-urifields are stripped of query parameters and fragments before logging. Script samples are truncated to 100 characters. - Do not execute reported scripts — the endpoint never evaluates
script-sampleorblocked-uricontent to prevent reflected XSS through the reporting mechanism itself.
- Legacy format (
application/csp-report):{ "csp-report": { ... } } - Modern Reporting API (
application/reports+json):[{ type: "csp-violation", body: { ... } }]
For production-grade alerting, integrate the CSP report endpoint with:
- Sentry — forward sanitized reports to Sentry's security monitoring.
- Datadog / CloudWatch Logs — centralize violation logs for dashboard visualization.
- PagerDuty — trigger alerts when violation rate exceeds thresholds.
The endpoint currently logs to console.warn; replace with lib/logger or your logging service in production.
The middleware injects a per-request CSP nonce so inline <script nonce="…"> tags (used by Next.js for the theme-bootstrap snippet) are allow-listed without requiring 'unsafe-inline' in production.
- Generation —
middleware.tscallscrypto.randomUUID()for each request to produce a cryptographically random UUID nonce. - Forwarding — The nonce is set on the forwarded request headers as
x-nonce.app/layout.tsxreads this header and attaches it to inline<script>tags. - CSP injection — If
next.config.jshas already set aContent-Security-Policyheader, the middleware replaces thescript-srcdirective with a nonce-scoped version, discarding the static placeholder.
| Environment | script-src |
|---|---|
| production | 'self' 'nonce-<uuid>' 'strict-dynamic' |
| development | 'self' 'nonce-<uuid>' 'unsafe-inline' 'unsafe-eval' |
'unsafe-eval' and 'unsafe-inline' are development-only and gated on NODE_ENV inside buildNonceCsp(). They must not appear in production script-src.
The middleware matcher (config.matcher) explicitly excludes _next/static, _next/image, favicon.ico, icons/, wallets/, and manifest.json so the nonce injection is never applied to static file responses.
| File | Role |
|---|---|
middleware.ts |
buildNonceCsp(nonce) and injection logic |
app/layout.tsx |
Reads x-nonce header and attaches to inline scripts |
__tests__/middleware.test.ts |
Regression tests: nonce UUID format, per-request entropy, prod/dev CSP profiles, matcher exclusions |
e2e/security-headers.spec.ts |
E2E: Content-Security-Policy header present on every page |