Security guidelines for contributors and operators of the Brain-Storm platform.
- Authentication & Authorization
- Input Validation & Sanitization
- Secrets Management
- OWASP Top 10 Mitigations
- Security Testing Procedures
Brain-Storm uses JWT (JSON Web Tokens) via @nestjs/passport and passport-jwt.
Best practices:
- Use a strong, randomly generated
JWT_SECRET(minimum 32 characters). Never reuse secrets across environments. - Set short token expiry (
JWT_EXPIRATION=15m) and implement refresh token rotation. - Always validate the
algheader — reject tokens usingalg: none. - Store JWTs in
httpOnly,Secure,SameSite=Strictcookies on the frontend. AvoidlocalStorage.
// apps/backend — JWT strategy example
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '15m', algorithm: 'HS256' },
});Roles (Admin, Instructor, Student) are enforced at both the API layer (NestJS RolesGuard) and on-chain (Soroban shared contract).
Best practices:
- Apply
@Roles()and@UseGuards(JwtAuthGuard, RolesGuard)to every protected route. Never rely on frontend-only role checks. - Follow the principle of least privilege — grant only the minimum role required.
- Validate on-chain RBAC for any action that mutates contract state.
@Get('admin/users')
@Roles(Role.Admin)
@UseGuards(JwtAuthGuard, RolesGuard)
getAllUsers() { ... }- Never expose the
STELLAR_SECRET_KEYto the frontend or logs. - Verify Stellar transaction signatures server-side before issuing credentials.
- Use testnet keys during development; rotate mainnet keys immediately if compromised.
Use class-validator and class-transformer with the global ValidationPipe.
// main.ts
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // strip unknown properties
forbidNonWhitelisted: true,
transform: true,
})
);Best practices:
- Annotate every DTO with
class-validatordecorators (@IsString(),@IsEmail(),@MaxLength(), etc.). - Use
whitelist: trueto automatically strip properties not defined in the DTO. - Validate and sanitize all path params and query strings — never pass raw user input to database queries.
- Always use TypeORM's query builder or repository methods — never concatenate raw SQL strings.
- Use parameterized queries for any custom SQL:
// Safe
repo.createQueryBuilder('user').where('user.email = :email', { email }).getOne();
// Never do this
repo.query(`SELECT * FROM user WHERE email = '${email}'`);- React auto-escapes JSX output — never use
dangerouslySetInnerHTMLwith user-supplied content. - Sanitize any rich-text or markdown content with a library like
DOMPurifybefore rendering. - Validate all form inputs client-side as a UX aid, but always re-validate server-side.
- Validate all input arguments at the top of every contract function before any state mutation.
- Use Rust's type system and
require!/panic!macros to enforce invariants. - Reject zero-address or out-of-range values explicitly.
- Copy
.env.exampleto.envand populate locally. Never commit.envto version control. .envis listed in.gitignore— verify this before every commit.- Use separate
.envfiles per environment (development,staging,production).
- Store production secrets in a secrets manager (AWS Secrets Manager, HashiCorp Vault, or GitHub Actions encrypted secrets).
- Rotate
JWT_SECRETandSTELLAR_SECRET_KEYon a scheduled basis and immediately after any suspected exposure. - Limit secret access to only the services and IAM roles that require them.
| Variable | Sensitivity | Notes |
|---|---|---|
JWT_SECRET |
Critical | Min 32 chars, random |
STELLAR_SECRET_KEY |
Critical | Never log or expose |
DATABASE_PASSWORD |
High | Use strong password |
REDIS_PASSWORD |
High | Enable AUTH in Redis |
NEXT_PUBLIC_* |
Public | Safe to expose to browser |
- Do not log secrets, tokens, or passwords — audit log statements before merging.
- Do not hardcode secrets in source code, Dockerfiles, or CI config files.
- Do not share secrets over Slack, email, or unencrypted channels.
- Enforce
RolesGuardon every protected NestJS route (see §1). - Validate resource ownership before returning data (e.g., a student can only access their own progress).
- Deny by default — routes are private unless explicitly marked public.
- Use HTTPS in all environments. Redirect HTTP → HTTPS at the load balancer.
- Never store plaintext passwords — use
bcryptwith a cost factor ≥ 12. - Use TLS 1.2+ for all database and Redis connections.
- Use parameterized queries and TypeORM (see §2).
- Validate and whitelist all user input via
ValidationPipe. - Escape output in templates; rely on React's default escaping.
- Threat-model new features before implementation.
- Apply rate limiting on auth endpoints to prevent brute-force attacks:
// Using @nestjs/throttler
ThrottlerModule.forRoot([{ ttl: 60000, limit: 10 }]);- Remove all default credentials and unused endpoints before deploying.
- Set security headers via NestJS Helmet middleware:
import helmet from 'helmet';
app.use(helmet());- Disable stack traces in production (
NODE_ENV=production).
- Run
npm auditandcargo auditin CI on every push (seedocs/security.md). - Keep dependencies up to date — use Dependabot or Renovate for automated PRs.
- Review
cargo denyoutput for license and vulnerability policy violations.
- Enforce strong password policies (min 8 chars, mixed case, numbers).
- Implement account lockout after repeated failed login attempts.
- Use short-lived JWTs with refresh token rotation (see §1).
- Verify Stellar transaction signatures server-side before issuing credentials.
- Pin dependency versions in
package-lock.jsonandCargo.lock— commit both files. - Use GitHub Actions OIDC for CI/CD instead of long-lived static credentials.
- Log authentication events (login, logout, failed attempts) with timestamps and IP addresses.
- Do not log sensitive data (passwords, tokens, secret keys).
- Set up alerts for anomalous patterns (e.g., spike in 401/403 responses).
- Validate and allowlist any URLs accepted as user input before making server-side HTTP requests.
- Block requests to internal metadata endpoints (e.g.,
169.254.169.254) at the network level. - Use a dedicated HTTP client with timeout and redirect limits.
Before opening a pull request, verify:
- No secrets or credentials in the diff (
git diffreview) - All new routes have appropriate auth guards
- New DTOs use
class-validatordecorators -
npm auditpasses with no high/critical findings -
cargo auditpasses (for contract changes)
The following security checks run automatically on every PR (see .github/workflows/):
| Check | Tool | Failure Threshold |
|---|---|---|
| Dependency vulnerabilities (Node) | npm audit |
High/Critical |
| Dependency vulnerabilities (Rust) | cargo audit |
Any advisory |
| DAST scan | OWASP ZAP | High severity |
| Static analysis | ESLint security rules | Error level |
| Rust linting | cargo clippy |
Warnings as errors |
For features involving auth, payments, or credential issuance, request a manual security review:
- Tag the PR with the
security-reviewlabel. - Assign a reviewer with security expertise.
- Reviewer checks: threat model, access control logic, input handling, and secret usage.
- Run OWASP ZAP locally against a staging environment before major releases (see
docs/security.mdfor setup). - Document findings and resolutions in the PR or a linked issue.
- Re-scan after fixes to confirm resolution.
Do not open public GitHub issues for security vulnerabilities. Instead:
- Email the maintainers directly or use GitHub's private vulnerability reporting feature.
- Include: description, reproduction steps, affected versions, and suggested fix.
- Allow reasonable time for a fix before public disclosure.
For tooling-specific details (ZAP setup, cargo audit, SonarCloud), see docs/security.md.