Security

On this page 30

stx provides comprehensive security features to help you build secure applications by default. This page covers all security capabilities and best practices in the stx ecosystem.

Built-in Security Features

Cross-Site Scripting (XSS) Prevention

stx automatically escapes output by default:

<!-- Safe: automatically escaped -->
<div>{{ userInput }}</div>

<!-- Raw HTML: use with caution -->
<div>&#123;&#123;&#123; trustedHtml &#125;&#125;&#125;</div>

URL schemes in attributes

Escaping stops a value from breaking out of its attribute. It does nothing about what the attribute means: javascript:alert(1) contains no character an HTML escape touches, so escaping alone left <a href="{{ url }}"> clickable when the URL came from a database row or an API.

A value that lands where a URL attribute's scheme is read is checked, and a scheme that can run script is refused -- prefixed unsafe:, which is an unknown scheme, so the browser does nothing with it, and the value stays readable to whoever is debugging it:

<!-- href="unsafe:javascript:alert(1)" -->
<a href="{{ url }}">Profile</a>

<!-- Same for the components that write a URL for you -->
<StxLink :to="user.website">Website</StxLink>
<StxImage :src="row.avatar" alt="" />

<!-- And for a URL bound at runtime -->
<a x-href="profileUrl">Profile</a>

What is deliberately not checked matters as much:

CaseBehaviourWhy
title="{{ text }}", <p>{{ text }}</p>untouchednot a URL
href="/search?q={{ term }}"untoucheda scheme can only be at the start of a URL
{!! url !!}untouchedthe raw form is how a template says it vouches for a value
src="data:image/..."still loadsinline images are a normal pattern; only data URLs carrying a document are refused

Refused schemes: javascript:, vbscript:, livescript:, mocha:, view-source:, and the data URLs that carry a document (data:text/html, data:application/xhtml+xml, plus data:image/svg+xml where it would be navigated to rather than loaded as an image). Spelling does not help an attacker: whitespace, control characters and case are normalised first, so java\tscript: and JavaScript: are read as what they are, and an entity-encoded spelling never becomes a scheme because the HTML escape runs first.

The rules live in one module, packages/stx/src/url-safety.ts, because four places need to agree on them: template interpolation, the signals runtime, <StxLink> and <StxImage>.

CSRF Protection

Built-in CSRF token generation and validation:

// Automatic CSRF protection
app.use(csrf({
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'strict'
  }
}))

Content Security Policy

Configure CSP headers:

app.use(csp({
  directives: {
    defaultSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    scriptSrc: ["'self'", "'nonce-{random}'"],
    imgSrc: ["'self'", "data:", "https:"]
  }
}))

Authentication & Authorization

Authentication Directives

stx provides built-in authentication directives for template-level access control:

<!-- Show content only to authenticated users -->
@auth
  <p>Welcome back, {{ user.name }}!</p>
@endauth

<!-- Show content only to guests -->
@guest
  <a href="/login">Sign in</a>
@endguest

<!-- Role-based access -->
@can('admin')
  <a href="/admin">Admin Panel</a>
@endcan

Session Security

// Secure session configuration
app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: {
    secure: true,
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
    sameSite: 'strict'
  },
  rolling: true
}))

Input Validation & Sanitization

Form Validation

import { validate } from '@stacksjs/validation'

const userSchema = {
  email: 'email|required',
  password: 'string|min:8|required',
  name: 'string|max:100|required'
}

app.post('/users', validate(userSchema), (req, res) => {
  // Validated data is safe to use
  const { email, password, name } = req.validated
})

SQL Injection Prevention

// Parameterized queries (safe)
const user = await db.query(
  'SELECT * FROM users WHERE email = ?',
  [email]
)

// ORM usage (safe)
const user = await User.where('email', email).first()

File Upload Security

import { upload } from '@stacksjs/upload'

app.post('/upload', upload({
  allowedTypes: ['image/jpeg', 'image/png'],
  maxSize: 5 * 1024 * 1024, // 5MB
  sanitizeFilename: true,
  virusScan: true
}))

Security Headers

Comprehensive Header Setup

app.use(helmet({
  // XSS Protection
  xssFilter: true,

  // Content Type Options
  noSniff: true,

  // Frame Options
  frameguard: { action: 'deny' },

  // HSTS
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  },

  // Referrer Policy
  referrerPolicy: 'strict-origin-when-cross-origin'
}))

Encryption & Hashing

Password Hashing

import { hash, verify } from '@stacksjs/crypto'

// Hash password
const hashedPassword = await hash.password(plainPassword, {
  algorithm: 'argon2id',
  memoryCost: 65536,
  timeCost: 3,
  parallelism: 4
})

// Verify password
const isValid = await verify.password(plainPassword, hashedPassword)

Data Encryption

import { encrypt, decrypt } from '@stacksjs/crypto'

// Encrypt sensitive data
const encrypted = await encrypt(sensitiveData, {
  algorithm: 'aes-256-gcm',
  key: process.env.ENCRYPTION_KEY
})

// Decrypt data
const decrypted = await decrypt(encrypted, {
  key: process.env.ENCRYPTION_KEY
})

Rate Limiting

Request Rate Limiting

import { rateLimit } from '@stacksjs/security'

// Global rate limiting
app.use(rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP'
}))

// API-specific rate limiting
app.use('/api', rateLimit({
  windowMs: 1 * 60 * 1000, // 1 minute
  max: 10 // 10 requests per minute
}))

Brute Force Protection

import { bruteForce } from '@stacksjs/security'

app.post('/login', bruteForce.protect(), async (req, res) => {
  // Login logic with automatic brute force protection
})

Security Monitoring

Security Event Logging

import { securityLogger } from '@stacksjs/logging'

// Log security events
securityLogger.warn('Failed login attempt', {
  ip: req.ip,
  userAgent: req.get('User-Agent'),
  email: req.body.email
})

// Log successful authentication
securityLogger.info('User authenticated', {
  userId: user.id,
  ip: req.ip
})

Intrusion Detection

import { ids } from '@stacksjs/security'

// Configure intrusion detection
app.use(ids({
  enabled: true,
  rules: [
    'detect_sql_injection',
    'detect_xss_attempts',
    'detect_path_traversal'
  ],
  onDetection: (threat) => {
    securityLogger.alert('Security threat detected', threat)
  }
}))

Security Testing

Automated Security Scans

# Run security audit
bun audit

# Run SAST (Static Application Security Testing)
bun run security:scan

# Run dependency vulnerability check
bun run security:deps

Penetration Testing

// Security test suite
import { securityTest } from '@stacksjs/testing'

securityTest('XSS protection', () => {
  const maliciousInput = '<script>alert("xss")</script>'
  const result = render(template, { input: maliciousInput })
  expect(result).not.toContain('<script>')
})

securityTest('CSRF protection', async () => {
  const response = await request(app)
    .post('/api/users')
    .send({ name: 'Test' })

  expect(response.status).toBe(403) // CSRF token missing
})

Compliance & Standards

OWASP Top 10 Protection

stx addresses the OWASP Top 10 security risks:

  1. Injection: Parameterized queries and input validation
  2. Broken Authentication: Secure session management
  3. Sensitive Data Exposure: Built-in encryption utilities
  4. XML External Entities: Safe XML parsing
  5. Broken Access Control: Role-based authorization
  6. Security Misconfiguration: Secure defaults
  7. Cross-Site Scripting: Automatic output escaping
  8. Insecure Deserialization: Safe serialization practices
  9. Known Vulnerabilities: Automated dependency scanning
  10. Insufficient Logging: Comprehensive security logging

Data Protection Compliance

// GDPR compliance helpers
import { gdpr } from '@stacksjs/compliance'

// Data anonymization
const anonymized = gdpr.anonymize(userData)

// Right to be forgotten
await gdpr.deleteUserData(userId)

// Data export
const exportData = gdpr.exportUserData(userId)