Skip to main content

Security

Spritz implements comprehensive security measures to protect users from malicious content, XSS attacks, and impersonation attempts.

URL Security​

User-generated content often includes URLs (profile links, widget embeds, social links). Spritz validates all URLs to prevent security vulnerabilities.

Dangerous URL Detection​

The isDangerousUrl function blocks potentially malicious URL schemes:

export function isDangerousUrl(url: string): boolean {
if (!url) return true;

const trimmed = url.trim().toLowerCase();

// Block dangerous protocols
if (trimmed.startsWith('javascript:')) return true;
if (trimmed.startsWith('data:') && !trimmed.startsWith('data:image/')) return true;
if (trimmed.startsWith('vbscript:')) return true;
if (trimmed.startsWith('file:')) return true;

// Block URLs with encoded dangerous characters
const decoded = decodeURIComponent(trimmed);
if (decoded.startsWith('javascript:')) return true;

return false;
}

Blocked protocols:

  • javascript: - Prevents XSS attacks
  • data: - Blocked except for data:image/ (base64 images)
  • vbscript: - Legacy but still dangerous
  • file: - Prevents local file access

URL Sanitization​

All URLs are sanitized before use:

export function sanitizeUrl(url: string | undefined | null): string | null {
if (!url) return null;

const trimmed = url.trim();

if (isDangerousUrl(trimmed)) {
console.warn('[Security] Blocked dangerous URL:', trimmed.substring(0, 50));
return null;
}

if (!isSafeProtocol(trimmed)) {
console.warn('[Security] Blocked non-http(s) URL:', trimmed.substring(0, 50));
return null;
}

return trimmed;
}

Social Platform Validation​

Social links are validated against known domains to prevent phishing:

const SOCIAL_DOMAINS: Record<string, string[]> = {
twitter: ['twitter.com', 'x.com'],
github: ['github.com'],
linkedin: ['linkedin.com', 'www.linkedin.com'],
instagram: ['instagram.com', 'www.instagram.com'],
youtube: ['youtube.com', 'www.youtube.com', 'youtu.be'],
tiktok: ['tiktok.com', 'www.tiktok.com'],
discord: ['discord.gg', 'discord.com'],
telegram: ['t.me', 'telegram.me'],
farcaster: ['warpcast.com'],
website: [], // Allow any for generic website
};

export function validateSocialUrl(platform: string, url: string): boolean {
const allowedDomains = SOCIAL_DOMAINS[platform.toLowerCase()];

// If no domain restriction, just check it's safe
if (!allowedDomains || allowedDomains.length === 0) {
return !isDangerousUrl(url) && isSafeProtocol(url);
}

try {
const parsed = new URL(url);
const hostname = parsed.hostname.toLowerCase().replace(/^www\./, '');

return allowedDomains.some(domain =>
hostname === domain || hostname === `www.${domain}`
);
} catch {
return false;
}
}

Embed URL Validation​

Video and music embeds are restricted to trusted platforms:

const EMBED_DOMAINS: Record<string, string[]> = {
spotify: ['open.spotify.com'],
youtube: ['youtube.com', 'www.youtube.com', 'youtube-nocookie.com'],
vimeo: ['player.vimeo.com', 'vimeo.com'],
loom: ['loom.com', 'www.loom.com'],
twitter: ['twitter.com', 'x.com', 'platform.twitter.com'],
instagram: ['instagram.com', 'www.instagram.com'],
};

Image URL Sanitization​

Image URLs support both regular URLs and base64 data URLs:

export function sanitizeImageUrl(
url: string | undefined | null,
strict: boolean = false
): string | null {
if (!url) return null;

const trimmed = url.trim();

// Allow data:image URLs (for base64 encoded images)
if (trimmed.startsWith('data:image/')) {
// Limit size to prevent abuse
if (trimmed.length > 100000) {
console.warn('[Security] Blocked oversized data URL');
return null;
}
return trimmed;
}

if (isDangerousUrl(trimmed)) return null;
if (!isSafeProtocol(trimmed)) return null;

return trimmed;
}

Trusted image domains (optional strict mode):

  • CDNs: Unsplash, Imgur, Cloudinary, jsDelivr
  • Social: Twitter, GitHub avatars
  • NFT: IPFS, OpenSea, Alchemy
  • Spritz: app.spritz.chat, spritz.chat

Video URL Parsing​

Video URLs are converted to safe embed URLs:

export function sanitizeVideoUrl(
platform: 'youtube' | 'vimeo' | 'loom',
input: string
): { embedUrl: string; videoId: string } | null {
// YouTube: Extract video ID and create nocookie embed
// Returns: https://www.youtube-nocookie.com/embed/{videoId}

// Vimeo: Extract video ID
// Returns: https://player.vimeo.com/video/{videoId}

// Loom: Extract share ID
// Returns: https://www.loom.com/embed/{shareId}
}

When rendering external links, use safe props:

export function getSafeExternalLinkProps(url: string | null) {
if (!url || isDangerousUrl(url)) {
return {
href: '#',
onClick: (e: React.MouseEvent) => e.preventDefault(),
};
}

return {
href: url,
target: '_blank',
rel: 'noopener noreferrer nofollow',
};
}

Always include:

  • target="_blank" - Opens in new tab
  • rel="noopener noreferrer nofollow" - Prevents tab hijacking and referrer leaks

Input Sanitization​

All user inputs are sanitized before storage:

import { sanitizeInput, INPUT_LIMITS } from "@/lib/sanitize";

// Sanitize with length limits
const sanitizedName = sanitizeInput(username, INPUT_LIMITS.USERNAME);

Input Limits​

FieldMax Length
Username20 characters
Display Name50 characters
Bio500 characters
Message4000 characters
Agent Personality1000 characters

Reserved Usernames​

To prevent impersonation and scams, certain usernames are reserved:

Blocked Categories​

Brand names:

  • spritz, spritzapp, spritzchat, spritz_support, etc.

Official roles:

  • admin, administrator, support, moderator, staff, team

Security terms:

  • security, root, sysadmin, webmaster

Financial terms:

  • wallet, vault, treasury, payment, billing

Authority titles:

  • ceo, cto, founder, developer, owner

Scam prevention:

  • giveaway, airdrop, prize, verify, claim

Known figures:

  • vitalik, satoshi, elon

Blocked Patterns​

Prefixes that are blocked:

  • spritz_*, official_*, support_*, admin_*, mod_*, team_*

Suffixes that are blocked:

  • *_official, *_support, *_admin, *_verified

Implementation​

function isReservedUsername(username: string): boolean {
const normalized = username.toLowerCase();

// Direct match against reserved list
if (RESERVED_USERNAMES.has(normalized)) {
return true;
}

// Check blocked prefixes
const reservedPrefixes = ["spritz_", "official_", "support_"];
for (const prefix of reservedPrefixes) {
if (normalized.startsWith(prefix)) return true;
}

// Check blocked suffixes
const reservedSuffixes = ["_official", "_support", "_admin", "_verified"];
for (const suffix of reservedSuffixes) {
if (normalized.endsWith(suffix)) return true;
}

return false;
}

Contract Address Validation​

For NFT widgets and Web3 features:

export function isValidContractAddress(address: string): boolean {
return /^0x[a-fA-F0-9]{40}$/.test(address);
}

Centralized Logging​

Spritz uses a centralized logger to control log output:

import { createLogger } from "@/lib/logger";

const authLogger = createLogger('Auth');

// In production: only errors logged
// In development: all logs shown
// Debug mode: localStorage.setItem('spritz_debug', 'true')

authLogger.debug('Debug message'); // Only in dev or debug mode
authLogger.info('Info message'); // Only in dev or debug mode
authLogger.warn('Warning'); // Always logged
authLogger.error('Error'); // Always logged

Pre-configured Loggers​

import { 
authLogger,
chatLogger,
callLogger,
walletLogger,
apiLogger
} from "@/lib/logger";

Enabling Debug Mode​

In browser console:

localStorage.setItem('spritz_debug', 'true');
// Reload page to see debug logs

Session Security​

Session Timeout & Lock Screen​

Spritz implements automatic session locking to protect users who leave their devices unattended.

Default Timeouts:

ScenarioTimeout
Active session (no activity)15 minutes
App backgrounded1 minute
Warning before lock1 minute
import { useInactivityMonitor, useSessionLock } from '@/hooks/useInactivityMonitor';

function App() {
const { lock, unlock, isLocked, lockReason } = useSessionLock();

const { resetActivity, isWarningShown, timeRemaining } = useInactivityMonitor({
timeout: 15 * 60 * 1000, // 15 minutes
backgroundTimeout: 60 * 1000, // 1 minute when backgrounded
warningThreshold: 60 * 1000, // Show warning 1 min before lock
onInactive: () => lock('inactivity'),
onWarning: () => console.log('Session will lock soon'),
});

// Show lock screen when session is locked
if (isLocked) {
return <SessionLockScreen onUnlock={unlock} reason={lockReason} />;
}

// Show warning banner when about to lock
if (isWarningShown && timeRemaining) {
return <div>Session locking in {timeRemaining}s</div>;
}

return <MainApp />;
}

Unlock Options:

  • Re-authenticate with passkey
  • Sign with connected wallet
  • Manual unlock (if recently authenticated)

CSRF Protection​

Sessions include origin validation:

// Validate request origin matches expected domains
const allowedOrigins = [
'https://app.spritz.chat',
'https://spritz.chat',
];

if (!allowedOrigins.includes(request.headers.get('origin'))) {
return new Response('Invalid origin', { status: 403 });
}

HttpOnly Cookies​

Session tokens are stored in HttpOnly cookies:

response.cookies.set('spritz_session', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7, // 7 days
path: '/',
});

Token Expiration​

  • Session tokens: 7 days
  • Passkey challenges: 5 minutes
  • Verification codes: 10 minutes
  • SIWE nonces: 5 minutes

Nonce Verification (Fail Closed)​

SIWE/SIWS nonces are stored in Redis and verified atomically (one-time use):

// Production behavior: FAIL CLOSED
if (!redis && isProduction) {
console.error("CRITICAL: Redis not configured in production!");
return false; // Reject auth request
}

// Development: Allow without verification (with warning)
if (!redis) {
console.warn("Redis not configured - nonce verification disabled (dev mode)");
return true;
}

Security Requirements:

  • UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN must be set in production
  • Without Redis, all authentication requests are rejected in production
  • This prevents replay attacks when nonce storage is unavailable
EnvironmentRedis AvailableBehavior
Production✅ YesNormal verification
Production❌ NoReject all auth
Development✅ YesNormal verification
Development❌ NoAllow (with warning)

Cryptographically Secure Randomness​

Spritz uses crypto.getRandomValues() for all security-sensitive random generation. Never use Math.random() for security purposes.

Available Functions​

import { 
secureRandomString,
secureVerificationCode,
secureRandomHex,
secureUUID,
secureRecoveryToken,
secureInviteCode,
} from '@/lib/secureRandom';

Verification Codes​

6-digit numeric codes with rejection sampling to avoid modulo bias:

const code = secureVerificationCode();
// Returns: "847291" (6 digits, always padded)

Uses rejection sampling: if the random number falls in the "bias zone" (0.02% of cases), it retries for uniform distribution.

Invite Codes​

URL-safe codes without ambiguous characters (0, O, I, 1, L excluded):

const invite = secureInviteCode(8);
// Returns: "KJ7HNPRQ"

Recovery Tokens​

URL-safe base64 tokens for password reset, etc.:

const token = secureRecoveryToken(32);
// Returns: "aB3dEfGh1jKlMnOp..."

Hex Strings​

For cryptographic keys and hashes:

const hex = secureRandomHex(32);
// Returns: "0x8a7b3c..." (64 hex chars = 32 bytes)

Usage Guidelines​

Use CaseFunctionExample
Email verificationsecureVerificationCode()"847291"
Invite codessecureInviteCode(8)"KJ7HNPRQ"
Password resetsecureRecoveryToken(32)URL-safe string
Transaction noncessecureRandomHex(32)"0x..."
UUIDssecureUUID()RFC 4122 UUID

DO use Math.random() for:

  • UI animations
  • Non-security log IDs
  • Shuffle for display only

Best Practices​

For Widget Content​

  1. Always sanitize URLs before rendering
  2. Validate embed domains for video/audio content
  3. Limit data URL sizes for images
  4. Use safe link props for external links

For User Input​

  1. Apply length limits to all text inputs
  2. Sanitize HTML (strip dangerous tags)
  3. Validate usernames against reserved list
  4. Normalize addresses (lowercase for EVM)

For API Endpoints​

  1. Verify session for authenticated routes
  2. Check origin headers for CSRF protection
  3. Rate limit to prevent abuse
  4. Log security events for audit trail

Security Headers​

Recommended headers for production:

const securityHeaders = {
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'",
'X-Frame-Options': 'DENY',
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
};

Security Checklist​

Before deploying, verify:

  • UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN are set
  • JWT secret is cryptographically strong (32+ bytes)
  • HttpOnly cookies are enabled for sessions
  • CORS is configured for allowed origins only
  • Rate limiting is enabled on API routes
  • RLS policies are enabled on sensitive tables
  • No sensitive data in client-side code or logs
  • Security headers are configured in production

Reporting Security Issues​

If you discover a security vulnerability:

  1. Do NOT open a public issue
  2. Email security@spritz.chat with details
  3. Include steps to reproduce
  4. Allow 90 days for fix before disclosure

We take security seriously and will acknowledge reports within 48 hours.