Skip to main content

Overview

Tresa Contafy implements multiple layers of security to protect user data and prevent common web vulnerabilities. This guide covers all security features and configuration.

Security Features

Helmet.js

HTTP security headers

Rate Limiting

DDoS and brute-force protection

CORS

Cross-origin request control

JWT

Secure token-based authentication

HTTP Security Headers (Helmet)

Configuration

Helmet is enabled by default in src/server.ts:38:
import helmet from 'helmet';

app.use(helmet());

Headers Applied

Helmet automatically sets secure HTTP headers:
Prevents XSS attacks by controlling resource loading:
Content-Security-Policy: default-src 'self'
Prevents MIME type sniffing:
X-Content-Type-Options: nosniff
Prevents clickjacking attacks:
X-Frame-Options: DENY
Enforces HTTPS connections:
Strict-Transport-Security: max-age=15552000; includeSubDomains
Controls DNS prefetching:
X-DNS-Prefetch-Control: off

Custom Helmet Configuration

For custom CSP or other headers:
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        scriptSrc: ["'self'"],
        imgSrc: ["'self'", "data:", "https:"],
      },
    },
  })
);

Rate Limiting

General API Rate Limit

Protects against DDoS attacks (src/server.ts:67):
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: process.env.NODE_ENV === 'production' ? 1000 : 2000,
  message: 'Too many requests, please try again later.',
  standardHeaders: true,
  legacyHeaders: false,
  skip: (req) => req.method === 'OPTIONS',
});

app.use('/api/', limiter);
windowMs
number
default:"900000"
Time window in milliseconds (15 minutes)
max
number
default:"1000 (prod) / 2000 (dev)"
Maximum requests per IP per window
standardHeaders
boolean
default:"true"
Return rate limit info in RateLimit-* headers

Authentication Rate Limit

Stricter limits for auth endpoints (src/server.ts:80):
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: process.env.NODE_ENV === 'production' ? 5 : 100,
  message: 'Too many authentication attempts, please try again later.',
  skipSuccessfulRequests: true,
});

app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);
app.use('/api/auth/google', authLimiter);
app.use('/api/auth/request-password-reset', authLimiter);
Production: 5 attempts per 15 minutesDevelopment: 100 attempts per 15 minutes (for testing)

Rate Limit Headers

Clients receive rate limit information:
RateLimit-Limit: 1000
RateLimit-Remaining: 999
RateLimit-Reset: 1709820000

Customizing Rate Limits

Adjust via environment variable:
API_RATE_LIMIT_MAX=2000

CORS Configuration

Default CORS Settings

CORS is configured based on environment (src/server.ts:41):
const corsOptions = {
  origin:
    process.env.FRONTEND_URL ||
    process.env.APP_URL ||
    (process.env.NODE_ENV === 'production' ? false : 'http://localhost:3000'),
  credentials: true,
  optionsSuccessStatus: 200,
};

app.use(cors(corsOptions));

Environment-Based CORS

# Allows localhost:3000
NODE_ENV=development
CORS allows: http://localhost:3000
In production without FRONTEND_URL or APP_URL, CORS is disabled (all origins blocked) for security.

Multiple Origins

To allow multiple origins:
const allowedOrigins = [
  'https://app.tresacontafy.com',
  'https://admin.tresacontafy.com',
];

const corsOptions = {
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
};

JWT Authentication

Token Configuration

JWT tokens are used for authentication with short-lived access tokens:
Access Token
string
Expires: 15 minutesSecret: JWT_SECRETUsage: API authentication
Refresh Token
string
Expires: 7 daysSecret: JWT_REFRESH_SECRETUsage: Obtaining new access tokens

Token Security

1

Use Strong Secrets

Generate cryptographically secure secrets:
# Generate 256-bit secret
openssl rand -base64 32
2

Rotate Secrets Regularly

Change JWT secrets periodically (every 90 days recommended)
Changing secrets invalidates all existing tokens
3

Store Tokens Securely

Client-side best practices:
  • Store in httpOnly cookies (recommended)
  • Or use secure localStorage with XSS protection
  • Never expose tokens in URLs
4

Implement Token Refresh

Use refresh tokens to obtain new access tokens:
POST /api/auth/refresh
{
  "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}

Password Security

Bcrypt Hashing

Passwords are hashed with bcrypt (10 salt rounds):
import bcrypt from 'bcrypt';

const hashedPassword = await bcrypt.hash(password, 10);

Password Requirements

Implement strong password requirements in your frontend:
  • Minimum 8 characters
  • At least one uppercase letter
  • At least one lowercase letter
  • At least one number
  • At least one special character

Password Reset Security

  1. Tokens expire: Password reset tokens expire after a set time
  2. One-time use: Tokens are invalidated after successful reset
  3. Rate limited: Reset requests are rate limited to prevent abuse

Email Verification

Required Verification

Users must verify their email before accessing protected resources:
POST /api/auth/verify-email
{
  "token": "verification-token-from-email"
}

Verification Token Security

  • Tokens are cryptographically random
  • Tokens expire after 24 hours
  • One-time use only
  • Stored hashed in database

Input Validation

Express Validator

All inputs are validated using express-validator:
import { body, validationResult } from 'express-validator';

const validateRegister = [
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 8 }),
  body('name').trim().notEmpty(),
];

router.post('/register', validateRegister, async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  // Process registration
});

SQL Injection Protection

Sequelize ORM provides automatic protection against SQL injection:
// Safe: Parameterized query
await User.findOne({ where: { email: userEmail } });

// Unsafe: Raw SQL (avoid)
await sequelize.query(`SELECT * FROM users WHERE email = '${userEmail}'`);

Row-Level Security

User Data Isolation

All user data is isolated by user_id:
// Only return profiles for authenticated user
const profiles = await Profile.findAll({
  where: { user_id: req.user.id },
});

Authorization Middleware

Verify resource ownership before operations:
const profile = await Profile.findByPk(profileId);

if (profile.user_id !== req.user.id) {
  return res.status(403).json({ error: 'Forbidden' });
}

File Upload Security

File Size Limits

Uploads are limited to 10MB (src/server.ts:105):
app.use(
  fileUpload({
    limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
    abortOnLimit: true,
  })
);

File Type Validation

Validate file types for XML uploads:
if (!file.name.endsWith('.xml')) {
  return res.status(400).json({ error: 'Only XML files allowed' });
}

Error Handling

Production Error Messages

Error details are hidden in production (src/server.ts:144):
app.use((err, req, res, next) => {
  logger.error({ err, method: req.method, path: req.path });
  
  const isDevelopment = process.env.NODE_ENV !== 'production';
  
  res.status(500).json({
    error: 'Internal server error',
    message: isDevelopment
      ? err.message
      : 'An unexpected error occurred.',
    ...(isDevelopment && { stack: err.stack }),
  });
});
Never expose stack traces or internal error details in production

Security Best Practices

1

Keep Dependencies Updated

Regularly update dependencies to patch vulnerabilities:
pnpm update
pnpm audit
2

Use HTTPS Only

Always use HTTPS in production:
  • Configure SSL certificates
  • Enable HSTS headers (via Helmet)
  • Redirect HTTP to HTTPS
3

Monitor Security Logs

Watch for suspicious activity:
  • Failed authentication attempts
  • Rate limit violations
  • Unusual traffic patterns
4

Regular Security Audits

Perform periodic security reviews:
  • Run pnpm audit
  • Review access logs
  • Test authentication flows
  • Penetration testing
5

Implement CSP

Configure Content Security Policy for your frontend
6

Enable Database Encryption

Use PostgreSQL SSL/TLS for data in transit

Security Checklist

✓ Helmet.js enabled for security headers
✓ Rate limiting configured (general + auth endpoints)
✓ CORS restricted to known origins
✓ Strong JWT secrets (32+ bytes random)
✓ Passwords hashed with bcrypt
✓ Email verification required
✓ Input validation on all endpoints
✓ SQL injection protection (Sequelize ORM)
✓ Row-level security (user_id isolation)
✓ File upload limits (10MB max)
✓ Error messages sanitized in production
✓ HTTPS enforced (HSTS header)
✓ Database SSL enabled
✓ Dependencies regularly updated

Incident Response

If a security incident occurs:
1

Identify & Contain

  • Identify affected systems
  • Isolate compromised resources
  • Block malicious IPs if needed
2

Investigate

  • Review logs for attack vector
  • Determine scope of breach
  • Identify compromised data
3

Remediate

  • Patch vulnerabilities
  • Rotate compromised secrets
  • Force password resets if needed
4

Notify

  • Inform affected users
  • Report to authorities if required
  • Document incident for review

Next Steps

Monitoring

Set up monitoring and logging

Production Deployment

Deploy to production environment

Build docs developers (and LLMs) love