Security

This document outlines security patterns, best practices, and implementation details for FT Configs UI.

Authentication

  • The UI relies on backend sessions and performs authenticated requests with withCredentials: true.

  • 401 responses trigger a POST /auth/refresh-token request; successful refresh replays the original request (tracked via a _retry flag to prevent infinite loops), failure redirects the browser to /login.

  • Refresh is skipped for requests to /auth/* — a 401 from the auth namespace is returned as-is.

  • Authentication state lives in AuthProvider (React Context). Route protection is enforced at the component level in src/routes/_protected.tsx (see Routing & Layouts).

Implementation reference: src/lib/axios.ts (request/response interceptors), src/contexts/auth-provider.tsx.

Session lifecycle

session-lifecycle

Password requirements

Passwords must meet complexity requirements enforced by Zod validation:

  • Length: 6-16 characters

  • Uppercase: At least one uppercase letter (A-Z)

  • Lowercase: At least one lowercase letter (a-z)

  • Digit: At least one number (0-9)

  • Special character: At least one of @$!%*?&

Example valid passwords: MyPass123!, Secure@2024, Admin$Pass1

Example invalid passwords:

  • short1! (too short)

  • NoDigitsOrSpecial (missing digit and special char)

  • no-uppercase123! (missing uppercase)

Implementation: createFirstLoginSchema() and createRegisterSchema() in src/lib/validation.ts:57-99

First-login flow

New users receive a temporary password via email (backend responsibility). On first login:

  1. User enters username and temporary password

  2. UI validates credentials and requests password rotation

  3. User enters new password (must meet complexity requirements)

  4. User confirms new password

  5. Backend validates match and creates permanent session

  6. User is redirected to application

Implementation: src/components/auth/first-login-page.tsx (rendered via the route file src/routes/_auth/change-password.tsx).

CSRF protection

CSRF is enforced in the Axios request interceptor (src/lib/axios.ts) — not in any server-side middleware. For every unsafe request the client performs a one-time GET /csrf exchange, then attaches the received token to the outgoing request.

Protected methods: POST, PUT, PATCH, DELETE.

Flow (per unsafe request):

  1. Check in-memory csrfTokenCache. If empty, dispatch GET /csrf with X-Requested-With: XMLHttpRequest and withCredentials: true.

  2. All concurrent unsafe requests share a single in-flight csrfPromise — the token fetch is deduplicated; only one network call per cache miss.

  3. Once resolved, attach to the outgoing request:

    • X-XSRF-TOKEN: <token>

    • X-Requested-With: XMLHttpRequest

  4. The backend also reads the XSRF-TOKEN cookie set during GET /csrf (double-submit pattern).

The token is cached for the lifetime of the page. A fresh GET /csrf only happens after a cache invalidation (e.g., on reload or after a 401 refresh path replays a request).

Implementation reference: csrfTokenCache, csrfPromise, and fetchCsrfToken() in src/lib/axios.ts.

Input validation strategy

All user inputs are validated using a layered approach:

Client-side validation (first line of defense)

Forms: React Hook Form + Zod schemas

import {useForm} from 'react-hook-form';
import {zodResolver} from '@hookform/resolvers/zod';
import {createLoginSchema} from '@/lib/validation';
import {useI18n} from '@/hooks/use-i18n';

const {t} = useI18n();
const loginSchema = createLoginSchema(t);

const form = useForm({
  resolver: zodResolver(loginSchema),
  defaultValues: {
    username: '',
    password: '',
  },
});

Direct inputs: Inline validation with Zod

import {z} from 'zod';

const portSchema = z.number().int().min(1).max(65535);

const validatePort = (value: number): boolean => {
  try {
    portSchema.parse(value);
    return true;
  } catch {
    return false;
  }
};

Benefits:

  • Immediate feedback to users

  • Reduces unnecessary backend calls

  • Localized error messages

API-level validation (server-side)

Backend validates all inputs again (never trust client):

  • Type validation (string, number, boolean, array)

  • Length/range constraints

  • Business logic validation (uniqueness, dependencies)

  • SQL injection prevention (parameterized queries)

Validation patterns

Username validation:

  • 3-50 characters

  • Alphanumeric, underscore, hyphen only: /^[a-zA-Z0-9_-]+$/

Email validation:

  • Standard email format

  • Max 100 characters

XML validation:

  • Well-formed XML check before parsing

  • No external entity expansion (XXE prevention)

Implementation: isWellFormedXml() in src/lib/xml.ts

File upload validation:

  • File type whitelist (MIME type + extension)

  • Max file size: 10 MB (configurable per endpoint)

  • Content validation (e.g., properties file structure, XML schema)

XSS prevention patterns

FT Configs UI follows React’s default XSS protection but adds extra layers:

React automatic escaping

React escapes all values rendered in JSX by default:

// Safe: React escapes userName
<div>{userName}</div>

// Safe: React escapes attributes
<input value={userInput} />

Dangerous patterns (avoid)

dangerouslySetInnerHTML: Only use for trusted content

// ⚠️ UNSAFE: Never use with user input
<div dangerouslySetInnerHTML={{__html: userInput}} />

// ✅ SAFE: Only for sanitized or trusted content
import DOMPurify from 'isomorphic-dompurify';

const sanitized = DOMPurify.sanitize(trustedHtml);
<div dangerouslySetInnerHTML={{__html: sanitized}} />

Direct DOM manipulation: Avoid when possible

// ❌ UNSAFE
element.innerHTML = userInput;

// ✅ SAFE
element.textContent = userInput;

URL handling

Always validate and sanitize URLs before rendering:

// ❌ UNSAFE: javascript: URLs can execute code
<a href={userProvidedUrl}>Link</a>

// ✅ SAFE: Validate protocol
const isSafeUrl = (url: string): boolean => {
  try {
    const parsed = new URL(url, window.location.origin);
    return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
  } catch {
    return false;
  }
};

const safeUrl = isSafeUrl(userProvidedUrl) ? userProvidedUrl : '#';
<a href={safeUrl}>Link</a>

API response sanitization

Treat all API responses as potentially malicious:

// ✅ SAFE: Validate response structure with Zod
import {z} from 'zod';

const userSchema = z.object({
  id: z.number(),
  username: z.string().max(50),
  email: z.string().email(),
});

const response = await apiService.get('/users/123');
const user = userSchema.parse(response.data); // Throws if invalid

Sensitive data handling

Passwords and credentials

Never log or expose:

  • Raw passwords

  • CSRF tokens

  • Session cookies

  • Refresh tokens

Implementation guidelines:

// ❌ UNSAFE: Logs password
console.log('User login:', {username, password});

// ✅ SAFE: Omit sensitive fields
console.log('User login:', {username});

// ❌ UNSAFE: Sends password in URL
await apiService.get(`/auth/check?password=${password}`);

// ✅ SAFE: Sends password in request body
await apiService.post('/auth/login', {username, password});

Masked input fields:

// Password fields
<Input type="password" value={password} onChange={...} />

// Secret reveal pattern
const [revealed, setRevealed] = useState(false);
<Input
  type={revealed ? 'text' : 'password'}
  value={secret}
  onChange={...}
/>
<Button onClick={() => setRevealed(!revealed)}>
  {revealed ? 'Hide' : 'Reveal'}
</Button>

Exported files with secrets

Warn users before exporting configuration containing sensitive data:

const handleExport = () => {
  showConfirmDialog({
    title: 'Export contains sensitive data',
    description: 'The exported file contains passwords and secrets in plain text. Store it securely.',
    confirmText: 'Export anyway',
    onConfirm: async () => {
      const blob = await api.export();
      downloadFile(blob, 'config.xml');
    },
  });
};

Implementation example: src/components/acs-bulk-data/*

Local storage considerations

Avoid storing sensitive data in localStorage:

  • Passwords

  • Session tokens (use httpOnly cookies instead)

  • API keys

  • CSRF tokens

Acceptable localStorage usage:

  • User preferences (theme, language)

  • Draft forms (non-sensitive data only)

  • UI state (sidebar collapsed, etc.)

HTTP security headers (served by nginx)

Security headers for the SPA are applied by nginx, not by the application bundle. The production image is nginx:alpine and the header directives live in the nginx config templates at docker/ops/nginx.conf.template (HTTPS+HTTP) and docker/ops/nginx-http-only.conf.template (HTTP-only). docker/ops/entrypoint.sh picks the template at startup based on the presence of /etc/nginx/ssl/tls.crt + tls.key.

The following headers are emitted at the server block level:

# docker/ops/nginx.conf.template (excerpt)
add_header X-Frame-Options          "DENY"                 always;
add_header X-Content-Type-Options   "nosniff"              always;
add_header Referrer-Policy          "no-referrer"          always;
add_header X-XSS-Protection         "1; mode=block"        always;

# HSTS — only in the SSL template
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# Content-Security-Policy
add_header Content-Security-Policy "default-src 'self'; \
  script-src 'self' 'unsafe-inline' 'unsafe-eval'; \
  style-src 'self' 'unsafe-inline'; \
  img-src 'self' data: blob:; \
  font-src 'self' data:; \
  connect-src 'self'; \
  frame-ancestors 'none'; \
  base-uri 'self'; \
  form-action 'self';" always;
connect-src 'self' is sufficient because the backend is proxied through nginx under the same origin at /configs-service/ (see Docker & Deploy). No cross-origin fetches are issued at runtime.

'unsafe-inline' / 'unsafe-eval' are present for compatibility with current third-party bundles; removing them is tracked as a hardening task.

Changing headers requires editing the templates in docker/ops/ and rebuilding the image — no runtime toggles exist.

Source maps are blocked

Source maps are never served in production:

  1. The builder stage in docker/Dockerfile runs find dist -name '*.map' -delete after npm run build, so .map files do not exist in the image. vite.config.ts uses sourcemap: 'hidden', which does not reference them from bundles.

  2. As a belt-and-suspenders measure, nginx returns 404 for any request matching ~* \.map$.

This prevents accidental disclosure of original TypeScript sources or internal paths even if a map file were ever copied in by mistake.

Dependency security

npm audit

Run security audits regularly:

# Check for vulnerabilities
npm audit

# View details
npm audit --json

# Fix automatically (minor/patch updates)
npm audit fix

# Fix with breaking changes (use with caution)
npm audit fix --force

CI/CD integration:

  • Run npm audit in build pipeline

  • Fail builds on high/critical vulnerabilities

  • Review and approve moderate vulnerabilities

Dependabot configuration

Use GitHub Dependabot for automated dependency updates:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    reviewers:
      - "security-team"
    labels:
      - "dependencies"
      - "security"

Vulnerable package handling

When vulnerabilities are found:

  1. Assess severity: Critical/High → immediate fix, Low/Moderate → schedule fix

  2. Check for patches: Update to latest patch version

  3. Review breaking changes: Read changelog before major updates

  4. Test thoroughly: Run full test suite after updates

  5. Document decisions: If vulnerability is accepted (false positive, no fix available), document reasoning

Role-based access control (RBAC)

Roles and permissions

  • VIEWER — read-only access, can browse and export

  • EDITOR — VIEWER + create/edit/delete/import

  • ADMIN — EDITOR + user management

Roles & permissions

VIEWER

Read-only access. Can browse and export configurations.

EDITOR

VIEWER access + can edit configurations (create/update/delete/import).

ADMIN

EDITOR access + user management (create/edit/disable/delete users).

Frontend enforcement

import {useAuth} from '@/hooks/use-auth';

const {isReadOnly, user} = useAuth();

// Disable write actions
<Button disabled={isReadOnly} onClick={handleSave}>
  Save
</Button>

// Hide admin-only features
{user?.role === 'ADMIN' && (
  <Link href="/settings/users">Manage Users</Link>
)}

Backend enforcement (primary)

Never rely solely on frontend checks:

  • Backend must validate role for every write operation

  • API returns 403 Forbidden for insufficient permissions

  • Frontend checks are UX enhancement only

Read-only enforcement (multi-layer)

Read-only (VIEWER) protection exists on three independent layers. Any one of them would be sufficient defense-in-depth; together they guarantee the user cannot even dispatch a mutating request from the browser:

  1. Axios request interceptor (src/lib/axios.ts + src/lib/permissions.ts). Before any request leaves the page, shouldBlockRequestForReadOnly(method, url) evaluates the current role (set via setCurrentUserRole). If the role is VIEWER, the method is in UNSAFE_METHODS (post|put|patch|delete), and the URL is not in the allowedUnsafePrefixes allowlist, the promise rejects locally — the HTTP call is never made.

  2. AuthProvider.isReadOnly flag. Computed as role === 'VIEWER' and exposed via useAuth(). Feature components receive it through props and disable mutating affordances.

  3. UI disabling. Buttons, import/export controls, and form submissions pass disabled={isReadOnly} so that VIEWER users see a fully navigable, read-only experience without dead clicks.

Backend enforcement remains primary. All three client-side layers are UX and defense-in-depth. The backend still validates the caller’s role for every write operation and returns 403 Forbidden for any attempt.

Implementation reference: src/lib/permissions.ts, src/lib/axios.ts, src/contexts/auth-provider.tsx.

Allowlist exceptions:

  • /auth/login — required for login

  • /auth/first-login — first-time password rotation

  • /auth/register — account creation

  • /auth/change-password — password updates

  • /auth/logout — session termination

  • /auth/refresh-token — token refresh

  • /auth/me/locale — language preference

  • /service-api/import/dry-run — read-only preview

Security checklist

Before committing code

  • No hardcoded credentials or API keys

  • Sensitive data not logged to console

  • User inputs validated with Zod schemas

  • No dangerouslySetInnerHTML with user input

  • File uploads validated (type, size, content)

  • Error messages don’t leak sensitive info

  • CSRF token attached to unsafe requests

  • Read-only users blocked from write actions

Before deploying

  • Run npm audit and fix high/critical issues

  • All environment variables configured correctly

  • Backend CSRF and CORS configured properly

  • HTTPS enforced in production

  • Session cookies marked httpOnly and secure

  • Content-Security-Policy header configured (recommended)

  • Rate limiting enabled on backend

  • Sensitive endpoints require authentication

Regular maintenance

  • Weekly dependency updates (Dependabot)

  • Monthly security audit review

  • Quarterly penetration testing (if applicable)

  • Annual security training for developers

Common vulnerabilities to avoid

SQL Injection

Status: Handled by backend (parameterized queries)

Frontend responsibility: Validate input types before sending to backend.

Cross-Site Scripting (XSS)

Mitigation:

  • React automatic escaping

  • Avoid dangerouslySetInnerHTML

  • Sanitize HTML with DOMPurify if needed

  • Validate URLs before rendering

Cross-Site Request Forgery (CSRF)

Mitigation:

  • CSRF token on all unsafe methods

  • SameSite cookie attribute

  • Origin/Referer validation (backend)

Insecure Direct Object References (IDOR)

Mitigation:

  • Backend validates user owns requested resource

  • Frontend hides actions for inaccessible resources

  • No reliance on client-side authorization

Sensitive Data Exposure

Mitigation:

  • HTTPS in production

  • HttpOnly cookies for session tokens

  • Masked password fields

  • Warnings before exporting secrets

  • No sensitive data in localStorage

Broken Authentication

Mitigation:

  • Strong password requirements

  • Token refresh on 401

  • Automatic logout on session expiry

  • First-login password rotation

Security Misconfiguration

Mitigation:

  • Disable TypeScript build errors only in development

  • Remove debug flags in production

  • CSP and other security headers are served by nginx (docker/ops/nginx.conf.template)

  • Source maps deleted from the image and blocked by nginx (~* \.map$ → 404)

  • Regular dependency updates

Incident response

If a security vulnerability is discovered:

  1. Assess impact: Determine affected systems and data

  2. Contain: Deploy hotfix or disable affected feature

  3. Notify: Inform stakeholders and users if data breach

  4. Fix: Implement permanent fix and test thoroughly

  5. Review: Conduct post-mortem to prevent recurrence

  6. Document: Update security documentation and checklist