This document covers security guidelines for TeachLink backend developers, aligned with the OWASP Top 10.
Enforce authorization at the service layer, not just the route level. Use @Roles() and RolesGuard on every protected endpoint.
// ✅ Correct – guard applied at controller level
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN)
@Delete(':id')
remove(@Param('id') id: string) { ... }
// ❌ Wrong – no authorization guard
@Delete(':id')
remove(@Param('id') id: string) { ... }Never trust the client-supplied user ID for ownership checks. Always derive the owner from the JWT:
// ✅ Correct
async update(courseId: string, dto: UpdateCourseDto, @CurrentUser() user: User) {
const course = await this.coursesService.findOne(courseId);
if (course.instructorId !== user.id) throw new ForbiddenOperationException();
...
}- Use
EncryptionService(AES-256-GCM) for sensitive fields at rest. - Never store plaintext passwords. Use bcrypt with ≥ 10 rounds in production (
BCRYPT_ROUNDSenv var). - Transmit data over HTTPS only. Never log secrets, tokens, or PII.
// ✅ Encrypt sensitive data before persisting
const encrypted = this.encryptionService.encrypt(sensitiveValue);
entity.sensitiveField = JSON.stringify(encrypted);All database access goes through TypeORM parameterized queries. Never interpolate user input into raw SQL.
// ✅ Correct – parameterized
repo.findOne({ where: { email } });
// ❌ Wrong – raw string interpolation
repo.query(`SELECT * FROM users WHERE email = '${email}'`);For search/LIKE patterns, use the provided sanitizer:
import { sanitizeSqlLike } from '../common/utils/sanitization.utils';
const safe = sanitizeSqlLike(userInput);
repo.createQueryBuilder('u').where('u.name LIKE :q', { q: `%${safe}%` });- Validate all inputs with
class-validatorDTOs and the globalValidationPipe. - Apply the principle of least privilege: services only receive the data they need.
- Use idempotency keys (
@Idempotency()) on mutating endpoints to prevent duplicate operations.
// main.ts – global validation pipe is already configured
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));- All secrets come from environment variables, never from source code.
- Helmet is applied globally in
main.tsfor secure HTTP headers. - CORS is configured via
cors.config.ts; restrictoriginto known domains in production. - Disable debug endpoints (
/debug/*) in production via theENABLE_DEBUG_MODULEfeature flag.
- Pin dependency versions in
package.json(no open ranges like^or~for security-sensitive packages). - Run
npm auditin CI. The pipeline fails on high/critical vulnerabilities. - Review
scripts/scan-licenses.jsoutput before releasing.
- JWT tokens are short-lived. Refresh tokens are stored in Redis sessions (
SessionService). - Brute-force protection is provided by
ThrottleMiddlewareandCustomThrottleGuard. - Wallet-based login (Starknet) must verify the signature server-side before issuing a JWT.
- Invalidate sessions on password change and logout.
// ✅ Always verify JWT before trusting claims
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@CurrentUser() user: User) { ... }- Stripe webhook signatures are verified by
WebhookSecurityServiceusing HMAC-SHA256. - Replay attacks are blocked by a 5-minute timestamp window and a processed-event ID cache.
- Never deserialize untrusted data with
evalorFunction().
// WebhookSecurityService already handles this:
const result = await this.webhookSecurityService.verifyStripeWebhook(rawBody, signature);
if (!result.valid) throw new UnauthorizedException(result.reason);Use AuditLoggerService for all security-relevant events. Do not log raw passwords, tokens, or full credit card numbers.
await this.auditLogger.logAuth({
userId: user.id,
action: AuditAction.LOGIN,
ipAddress: req.ip,
success: true,
});Suspicious activity (repeated failures, unusual access patterns) is tracked by ThreatDetectionService and triggers alerts via AlertingService.
- Validate and allowlist URLs before making outbound HTTP requests.
- Never forward user-supplied URLs directly to
fetch/axioswithout validation. - Internal service URLs must come from environment configuration, not user input.
// ✅ Use allowlisted base URLs from config
const baseUrl = this.configService.get<string>('INTERNAL_SERVICE_URL');
await axios.get(`${baseUrl}/internal/resource`);
// ❌ Never do this
await axios.get(userSuppliedUrl);- Rotate JWT secrets periodically via
SecretsManagerService. - Store session state in Redis, not in JWTs, to allow instant revocation.
- Set
httpOnlyandSecureflags on session cookies. - Enforce MFA for admin roles.
- Every controller parameter must be typed with a DTO decorated with
class-validator. - Use
@IsUUID()for ID parameters to prevent injection via path params. - Strip unknown fields with
whitelist: trueonValidationPipe. - Sanitize HTML/markdown content before storage to prevent stored XSS.
- Global throttle is applied in
main.tsviaThrottleModule. - Per-user quota is enforced by
QuotaGuardusingQuotaTrackingService. - Adaptive rate limiting (
AdaptiveRateLimitingService) adjusts limits under load.
- PII fields are masked in API responses by
MaskingInterceptorbased on the caller's role. DataAnonymizationServiceis used for analytics exports.- GDPR data export and deletion are handled by
ComplianceService. - Audit logs retain PII only for the configured retention period (
AuditRetentionTask).
- Retrieve secrets at runtime via
SecretsManagerService(AWS Secrets Manager) orVaultSecretsService(HashiCorp Vault). - Never commit
.envfiles. Use.env.examplewith placeholder values. - Rotate secrets without downtime using the
/secrets/rotateendpoint (admin only).
@Controller('courses')
@UseGuards(JwtAuthGuard, RolesGuard, QuotaGuard)
@ApiHeader({ name: 'X-API-Version', required: true })
export class CoursesController {
@Post()
@Roles(Role.INSTRUCTOR)
@HttpCode(HttpStatus.CREATED)
async create(
@Body() dto: CreateCourseDto, // validated & whitelisted by global ValidationPipe
@CurrentUser() user: User,
) {
return this.coursesService.create(dto, user.id);
}
}import { IsString, IsNotEmpty, MaxLength, IsOptional, IsUrl } from 'class-validator';
import { Transform } from 'class-transformer';
export class CreateCourseDto {
@IsString()
@IsNotEmpty()
@MaxLength(200)
@Transform(({ value }) => value?.trim())
title: string;
@IsOptional()
@IsUrl()
thumbnailUrl?: string;
}// In a service method
await this.auditLogger.logSecurityEvent({
userId: user.id,
action: AuditAction.PERMISSION_DENIED,
resource: 'course',
resourceId: courseId,
ipAddress: request.ip,
details: { reason: 'not_owner' },
});// Storing
const payload = this.encryptionService.encrypt(walletPrivateKey);
user.encryptedKey = JSON.stringify(payload);
// Retrieving
const payload: IEncryptedPayload = JSON.parse(user.encryptedKey);
const privateKey = this.encryptionService.decrypt(payload);Use this checklist when reviewing a PR that touches security-sensitive code.
- All non-public endpoints have
@UseGuards(JwtAuthGuard) - Role-restricted endpoints have
@UseGuards(RolesGuard)and@Roles(...) - Ownership is verified from the JWT, not from a request body field
- Admin-only endpoints are tested with a non-admin token (expect 403)
- Every
@Body()parameter uses a DTO withclass-validatordecorators -
ValidationPipeis applied globally withwhitelist: true - Path/query params are typed (
@IsUUID(),@IsInt(), etc.) - User-supplied strings are not interpolated into raw SQL or shell commands
- No secrets, API keys, or passwords in source code or committed
.envfiles - New environment variables are documented in
.env.example - Secrets are loaded via
ConfigServiceorSecretsManagerService
- Passwords hashed with bcrypt (
BCRYPT_ROUNDS≥ 10 in production) - Sensitive fields encrypted with
EncryptionService(AES-256-GCM) - No use of MD5 or SHA-1 for security purposes
- Incoming webhook signatures verified before processing payload
- Replay attack prevention in place (timestamp + event ID check)
- Outbound HTTP requests use allowlisted base URLs from config
- Security events logged via
AuditLoggerService - No PII, passwords, or tokens in log output
- Sensitive operations decorated with
@SensitiveOperation()
- Mutating endpoints covered by throttle guard
- Expensive operations (search, export) have quota limits
- Idempotency key used on payment and enrollment endpoints
-
npm auditpasses with no high/critical findings - New packages are well-known and actively maintained
- Package versions are pinned for security-sensitive libraries
For questions or to report a vulnerability, contact the security team or open a confidential issue.