Security Headers Best Practices for ChatGPT Apps
HTTP security headers are your first line of defense against common web vulnerabilities when building ChatGPT apps. While OpenAI provides robust infrastructure security, developers must implement proper security headers to protect against cross-site scripting (XSS), clickjacking, man-in-the-middle attacks, and other client-side threats. Security headers instruct browsers to enforce security policies, preventing attackers from exploiting vulnerabilities even if your code has flaws. For ChatGPT apps handling sensitive user data, processing OAuth tokens, or rendering user-generated content, implementing comprehensive security headers is non-negotiable. This guide provides production-ready TypeScript implementations for securing your ChatGPT app with Helmet.js, Content Security Policy, HSTS, CORS, and additional protective headers.
Content Security Policy (CSP)
Content Security Policy is the most powerful security header for preventing XSS attacks. CSP defines which resources browsers can load, blocking malicious scripts even if they're injected into your page. For ChatGPT apps, a proper CSP prevents attackers from stealing OAuth tokens, hijacking user sessions, or manipulating AI responses.
Modern CSP implementations use nonce-based policies with strict-dynamic, eliminating the need for complex domain allowlists. Here's a production-ready CSP middleware:
// src/middleware/csp.ts
import { Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
interface CSPConfig {
reportUri?: string;
reportOnly?: boolean;
upgradeInsecureRequests?: boolean;
enableNonce?: boolean;
}
export class ContentSecurityPolicy {
private config: CSPConfig;
constructor(config: CSPConfig = {}) {
this.config = {
reportOnly: false,
upgradeInsecureRequests: true,
enableNonce: true,
...config
};
}
/**
* Generate cryptographically secure nonce for CSP
*/
private generateNonce(): string {
return crypto.randomBytes(16).toString('base64');
}
/**
* Build CSP directive string
*/
private buildCSP(nonce?: string): string {
const directives: string[] = [
"default-src 'self'",
nonce
? `script-src 'nonce-${nonce}' 'strict-dynamic' https: 'unsafe-inline'`
: "script-src 'self'",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https:",
"connect-src 'self' https://api.openai.com https://*.openai.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'"
];
if (this.config.upgradeInsecureRequests) {
directives.push('upgrade-insecure-requests');
}
if (this.config.reportUri) {
directives.push(`report-uri ${this.config.reportUri}`);
directives.push(`report-to csp-endpoint`);
}
return directives.join('; ');
}
/**
* CSP middleware for Express
*/
middleware() {
return (req: Request, res: Response, next: NextFunction) => {
if (this.config.enableNonce) {
const nonce = this.generateNonce();
res.locals.cspNonce = nonce;
const headerName = this.config.reportOnly
? 'Content-Security-Policy-Report-Only'
: 'Content-Security-Policy';
res.setHeader(headerName, this.buildCSP(nonce));
} else {
res.setHeader('Content-Security-Policy', this.buildCSP());
}
next();
};
}
/**
* Configure CSP violation reporting endpoint
*/
static reportingEndpoint(config: { group: string; max_age: number; endpoints: Array<{ url: string }> }) {
return JSON.stringify({
group: config.group,
max_age: config.max_age,
endpoints: config.endpoints
});
}
}
// Usage in Express app
import express from 'express';
const app = express();
const csp = new ContentSecurityPolicy({
reportUri: '/api/csp-report',
reportOnly: false,
upgradeInsecureRequests: true
});
app.use(csp.middleware());
// CSP violation reporting endpoint
app.post('/api/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.error('CSP Violation:', JSON.stringify(req.body, null, 2));
res.status(204).end();
});
The strict-dynamic directive allows nonce-tagged scripts to load additional scripts, simplifying CSP management while maintaining security. Always use frame-ancestors 'none' to prevent clickjacking attacks.
HSTS & Certificate Pinning
HTTP Strict Transport Security (HSTS) forces browsers to only connect via HTTPS, preventing protocol downgrade attacks and cookie hijacking. For ChatGPT apps handling OAuth flows, HSTS is critical to protect authentication tokens from man-in-the-middle attacks.
Here's a production-ready HSTS implementation with preload support:
// src/middleware/hsts.ts
import { Request, Response, NextFunction } from 'express';
interface HSTSConfig {
maxAge?: number;
includeSubDomains?: boolean;
preload?: boolean;
requireHttps?: boolean;
}
export class StrictTransportSecurity {
private config: Required<HSTSConfig>;
constructor(config: HSTSConfig = {}) {
this.config = {
maxAge: 31536000, // 1 year in seconds
includeSubDomains: true,
preload: true,
requireHttps: true,
...config
};
}
/**
* Build HSTS header value
*/
private buildHSTSHeader(): string {
const parts: string[] = [`max-age=${this.config.maxAge}`];
if (this.config.includeSubDomains) {
parts.push('includeSubDomains');
}
if (this.config.preload) {
parts.push('preload');
}
return parts.join('; ');
}
/**
* HSTS middleware for Express
*/
middleware() {
return (req: Request, res: Response, next: NextFunction) => {
// Only set HSTS on HTTPS connections
if (req.secure || !this.config.requireHttps) {
res.setHeader('Strict-Transport-Security', this.buildHSTSHeader());
} else if (this.config.requireHttps) {
// Redirect HTTP to HTTPS
return res.redirect(301, `https://${req.headers.host}${req.url}`);
}
next();
};
}
/**
* Validate HSTS preload eligibility
*/
static validatePreloadRequirements(domain: string, maxAge: number): {
eligible: boolean;
issues: string[];
} {
const issues: string[] = [];
if (maxAge < 31536000) {
issues.push('max-age must be at least 31536000 (1 year)');
}
if (!domain.match(/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}$/i)) {
issues.push('Invalid domain format for HSTS preload');
}
return {
eligible: issues.length === 0,
issues
};
}
}
// Usage
const hsts = new StrictTransportSecurity({
maxAge: 63072000, // 2 years
includeSubDomains: true,
preload: true,
requireHttps: true
});
app.use(hsts.middleware());
// Validate preload eligibility
const validation = StrictTransportSecurity.validatePreloadRequirements(
'chatgpt-app.example.com',
63072000
);
if (validation.eligible) {
console.log('✅ Domain eligible for HSTS preload list');
console.log('Submit at: https://hstspreload.org/');
} else {
console.error('❌ HSTS preload issues:', validation.issues);
}
Submit your domain to the HSTS preload list after deploying this configuration for maximum protection.
Frame Protection
Clickjacking attacks trick users into clicking malicious elements overlaid on your ChatGPT app. Frame protection headers prevent your app from being embedded in iframes, blocking these attacks.
Implement comprehensive frame protection with both legacy and modern headers:
// src/middleware/frame-protection.ts
import { Request, Response, NextFunction } from 'express';
type FrameOption = 'DENY' | 'SAMEORIGIN' | 'ALLOW-FROM';
interface FrameProtectionConfig {
action?: FrameOption;
allowFrom?: string;
enableCSPFrameAncestors?: boolean;
reportUri?: string;
}
export class FrameProtection {
private config: Required<Omit<FrameProtectionConfig, 'allowFrom'>> & { allowFrom?: string };
constructor(config: FrameProtectionConfig = {}) {
this.config = {
action: 'DENY',
enableCSPFrameAncestors: true,
reportUri: '/api/frame-violation',
...config
};
}
/**
* Set X-Frame-Options header (legacy support)
*/
private setXFrameOptions(res: Response): void {
if (this.config.action === 'ALLOW-FROM' && this.config.allowFrom) {
// ALLOW-FROM is deprecated but still used by older browsers
res.setHeader('X-Frame-Options', `ALLOW-FROM ${this.config.allowFrom}`);
} else {
res.setHeader('X-Frame-Options', this.config.action);
}
}
/**
* Set CSP frame-ancestors directive (modern approach)
*/
private setFrameAncestors(res: Response): void {
let directive: string;
switch (this.config.action) {
case 'DENY':
directive = "frame-ancestors 'none'";
break;
case 'SAMEORIGIN':
directive = "frame-ancestors 'self'";
break;
case 'ALLOW-FROM':
if (this.config.allowFrom) {
// Extract domain from URL
const domain = new URL(this.config.allowFrom).origin;
directive = `frame-ancestors 'self' ${domain}`;
} else {
directive = "frame-ancestors 'none'";
}
break;
default:
directive = "frame-ancestors 'none'";
}
// Append to existing CSP or create new header
const existingCSP = res.getHeader('Content-Security-Policy') as string;
if (existingCSP) {
res.setHeader('Content-Security-Policy', `${existingCSP}; ${directive}`);
} else {
res.setHeader('Content-Security-Policy', directive);
}
}
/**
* Frame protection middleware
*/
middleware() {
return (req: Request, res: Response, next: NextFunction) => {
// Set legacy X-Frame-Options for older browsers
this.setXFrameOptions(res);
// Set modern CSP frame-ancestors
if (this.config.enableCSPFrameAncestors) {
this.setFrameAncestors(res);
}
next();
};
}
/**
* Detect potential clickjacking attempts
*/
static detectClickjacking(req: Request): boolean {
const referer = req.headers.referer;
const host = req.headers.host;
if (!referer || !host) {
return false;
}
try {
const refererUrl = new URL(referer);
const refererHost = refererUrl.hostname;
// Check if referer is from different domain
return refererHost !== host && !refererHost.endsWith(`.${host}`);
} catch {
return false;
}
}
}
// Usage examples
// Example 1: Complete frame blocking
const frameProtection = new FrameProtection({
action: 'DENY',
enableCSPFrameAncestors: true
});
app.use(frameProtection.middleware());
// Example 2: Allow same-origin framing
const sameOriginFraming = new FrameProtection({
action: 'SAMEORIGIN',
enableCSPFrameAncestors: true
});
// Example 3: Allow specific domain (use with caution)
const allowTrustedDomain = new FrameProtection({
action: 'ALLOW-FROM',
allowFrom: 'https://trusted-partner.com',
enableCSPFrameAncestors: true
});
// Clickjacking detection endpoint
app.use((req, res, next) => {
if (FrameProtection.detectClickjacking(req)) {
console.warn('Potential clickjacking attempt detected:', {
referer: req.headers.referer,
host: req.headers.host,
ip: req.ip
});
}
next();
});
For ChatGPT apps, use action: 'DENY' unless you have specific requirements to embed your app in trusted iframes. The CSP frame-ancestors directive provides more granular control than X-Frame-Options.
CORS Configuration
Cross-Origin Resource Sharing (CORS) controls which domains can access your ChatGPT app's API. Misconfigured CORS is a common vulnerability that can expose sensitive data or enable CSRF attacks.
Here's a production-ready CORS middleware with preflight caching and credential handling:
// src/middleware/cors.ts
import { Request, Response, NextFunction } from 'express';
interface CORSConfig {
allowedOrigins?: string[] | string | ((origin: string) => boolean);
allowedMethods?: string[];
allowedHeaders?: string[];
exposedHeaders?: string[];
credentials?: boolean;
maxAge?: number;
optionsSuccessStatus?: number;
}
export class CORSMiddleware {
private config: Required<CORSConfig>;
constructor(config: CORSConfig = {}) {
this.config = {
allowedOrigins: '*',
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
exposedHeaders: ['X-Total-Count', 'X-Page-Count'],
credentials: true,
maxAge: 86400, // 24 hours
optionsSuccessStatus: 204,
...config
};
}
/**
* Check if origin is allowed
*/
private isOriginAllowed(origin: string | undefined): boolean {
if (!origin) return false;
const { allowedOrigins } = this.config;
// Allow all origins
if (allowedOrigins === '*') return true;
// Function-based validation
if (typeof allowedOrigins === 'function') {
return allowedOrigins(origin);
}
// String match
if (typeof allowedOrigins === 'string') {
return origin === allowedOrigins;
}
// Array of allowed origins
return allowedOrigins.includes(origin);
}
/**
* Set CORS headers
*/
private setCORSHeaders(req: Request, res: Response): void {
const origin = req.headers.origin;
// Set Access-Control-Allow-Origin
if (this.isOriginAllowed(origin)) {
if (this.config.credentials) {
// When credentials: true, must specify exact origin (not *)
res.setHeader('Access-Control-Allow-Origin', origin || '');
} else {
res.setHeader('Access-Control-Allow-Origin', this.config.allowedOrigins === '*' ? '*' : origin || '');
}
}
// Set Access-Control-Allow-Credentials
if (this.config.credentials) {
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
// Set Access-Control-Expose-Headers
if (this.config.exposedHeaders.length > 0) {
res.setHeader('Access-Control-Expose-Headers', this.config.exposedHeaders.join(', '));
}
// Set Vary header for proper caching
res.setHeader('Vary', 'Origin');
}
/**
* Handle preflight requests
*/
private handlePreflight(req: Request, res: Response): void {
this.setCORSHeaders(req, res);
// Set Access-Control-Allow-Methods
res.setHeader('Access-Control-Allow-Methods', this.config.allowedMethods.join(', '));
// Set Access-Control-Allow-Headers
const requestHeaders = req.headers['access-control-request-headers'];
if (requestHeaders) {
res.setHeader('Access-Control-Allow-Headers', requestHeaders);
} else {
res.setHeader('Access-Control-Allow-Headers', this.config.allowedHeaders.join(', '));
}
// Set Access-Control-Max-Age
res.setHeader('Access-Control-Max-Age', this.config.maxAge.toString());
res.status(this.config.optionsSuccessStatus).end();
}
/**
* CORS middleware
*/
middleware() {
return (req: Request, res: Response, next: NextFunction) => {
// Handle preflight requests
if (req.method === 'OPTIONS') {
return this.handlePreflight(req, res);
}
// Set CORS headers for actual requests
this.setCORSHeaders(req, res);
next();
};
}
}
// Usage examples
// Example 1: Production ChatGPT app (restrictive)
const productionCORS = new CORSMiddleware({
allowedOrigins: [
'https://chatgpt.com',
'https://platform.openai.com',
'https://your-app.com'
],
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
});
app.use(productionCORS.middleware());
// Example 2: Development environment (permissive)
const devCORS = new CORSMiddleware({
allowedOrigins: (origin: string) => {
return origin.includes('localhost') || origin.includes('127.0.0.1');
},
credentials: true
});
if (process.env.NODE_ENV === 'development') {
app.use(devCORS.middleware());
}
// Example 3: Public API (no credentials)
const publicCORS = new CORSMiddleware({
allowedOrigins: '*',
credentials: false,
exposedHeaders: ['X-RateLimit-Remaining', 'X-RateLimit-Reset']
});
app.use('/api/public', publicCORS.middleware());
Critical CORS security rules:
- Never use
allowedOrigins: '*'withcredentials: true(browsers will block this) - Always validate origin against allowlist for authenticated endpoints
- Set
maxAgeto cache preflight responses and reduce latency - Use
Vary: Originheader for proper CDN caching
Additional Security Headers
Beyond CSP, HSTS, frame protection, and CORS, several other headers provide defense-in-depth:
X-Content-Type-Options: Prevents MIME-sniffing attacks by forcing browsers to respect declared content types.
Referrer-Policy: Controls how much referrer information is sent with requests, protecting user privacy.
Permissions-Policy: Restricts access to browser features like geolocation, camera, and microphone.
Here's a comprehensive security headers implementation using Helmet.js:
// src/middleware/security-headers.ts
import helmet from 'helmet';
import { Request, Response, NextFunction } from 'express';
export class SecurityHeaders {
/**
* Complete security headers configuration using Helmet.js
*/
static helmet() {
return helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'strict-dynamic'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.openai.com", "https://*.openai.com"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"]
}
},
// HSTS
hsts: {
maxAge: 63072000, // 2 years
includeSubDomains: true,
preload: true
},
// X-Frame-Options
frameguard: {
action: 'deny'
},
// X-Content-Type-Options
noSniff: true,
// Referrer-Policy
referrerPolicy: {
policy: 'strict-origin-when-cross-origin'
},
// X-XSS-Protection (deprecated but still used by older browsers)
xssFilter: true,
// X-Download-Options
ieNoOpen: true,
// X-DNS-Prefetch-Control
dnsPrefetchControl: {
allow: false
},
// Permissions-Policy
permittedCrossDomainPolicies: {
permittedPolicies: 'none'
}
});
}
/**
* Permissions-Policy header (replaces Feature-Policy)
*/
static permissionsPolicy() {
return (req: Request, res: Response, next: NextFunction) => {
const policies = [
'geolocation=()',
'microphone=()',
'camera=()',
'payment=()',
'usb=()',
'magnetometer=()',
'gyroscope=()',
'accelerometer=()',
'interest-cohort=()' // Disable FLoC
];
res.setHeader('Permissions-Policy', policies.join(', '));
next();
};
}
/**
* Custom security headers
*/
static custom() {
return (req: Request, res: Response, next: NextFunction) => {
// Remove X-Powered-By header
res.removeHeader('X-Powered-By');
// Set custom security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
};
}
}
// Usage
import express from 'express';
const app = express();
// Apply all security headers
app.use(SecurityHeaders.helmet());
app.use(SecurityHeaders.permissionsPolicy());
app.use(SecurityHeaders.custom());
This configuration provides comprehensive protection against common web vulnerabilities while maintaining compatibility with ChatGPT's platform requirements.
For detailed CORS configuration specific to ChatGPT apps, see our guide on CORS Configuration for ChatGPT Apps. To protect against XSS attacks, check out XSS Prevention in ChatGPT Apps. For clickjacking defense strategies, refer to Clickjacking Prevention for ChatGPT Apps.
Security Header Validation
After implementing security headers, validate your configuration to ensure proper protection:
// src/utils/security-validator.ts
import axios from 'axios';
interface SecurityAudit {
url: string;
timestamp: Date;
headers: Record<string, string>;
score: number;
findings: Array<{
header: string;
status: 'pass' | 'warn' | 'fail';
message: string;
}>;
}
export class SecurityHeaderValidator {
private requiredHeaders = [
'Content-Security-Policy',
'Strict-Transport-Security',
'X-Frame-Options',
'X-Content-Type-Options',
'Referrer-Policy'
];
/**
* Audit security headers from live URL
*/
async audit(url: string): Promise<SecurityAudit> {
const response = await axios.get(url, {
validateStatus: () => true
});
const headers = response.headers;
const findings = this.validateHeaders(headers);
const score = this.calculateScore(findings);
return {
url,
timestamp: new Date(),
headers,
score,
findings
};
}
/**
* Validate individual headers
*/
private validateHeaders(headers: Record<string, string>): SecurityAudit['findings'] {
const findings: SecurityAudit['findings'] = [];
// Check CSP
const csp = headers['content-security-policy'];
if (!csp) {
findings.push({
header: 'Content-Security-Policy',
status: 'fail',
message: 'Missing Content-Security-Policy header'
});
} else if (!csp.includes('frame-ancestors')) {
findings.push({
header: 'Content-Security-Policy',
status: 'warn',
message: 'CSP missing frame-ancestors directive'
});
} else {
findings.push({
header: 'Content-Security-Policy',
status: 'pass',
message: 'CSP properly configured'
});
}
// Check HSTS
const hsts = headers['strict-transport-security'];
if (!hsts) {
findings.push({
header: 'Strict-Transport-Security',
status: 'fail',
message: 'Missing HSTS header'
});
} else {
const maxAge = hsts.match(/max-age=(\d+)/)?.[1];
const hasPreload = hsts.includes('preload');
const hasSubDomains = hsts.includes('includeSubDomains');
if (maxAge && parseInt(maxAge) >= 31536000 && hasPreload && hasSubDomains) {
findings.push({
header: 'Strict-Transport-Security',
status: 'pass',
message: 'HSTS properly configured with preload'
});
} else {
findings.push({
header: 'Strict-Transport-Security',
status: 'warn',
message: 'HSTS could be improved (use max-age >= 1 year, includeSubDomains, preload)'
});
}
}
// Check X-Frame-Options
const xfo = headers['x-frame-options'];
if (!xfo) {
findings.push({
header: 'X-Frame-Options',
status: 'warn',
message: 'Missing X-Frame-Options (use CSP frame-ancestors instead)'
});
} else if (xfo === 'DENY' || xfo === 'SAMEORIGIN') {
findings.push({
header: 'X-Frame-Options',
status: 'pass',
message: 'X-Frame-Options properly set'
});
}
// Check X-Content-Type-Options
if (headers['x-content-type-options'] !== 'nosniff') {
findings.push({
header: 'X-Content-Type-Options',
status: 'fail',
message: 'Missing or incorrect X-Content-Type-Options'
});
} else {
findings.push({
header: 'X-Content-Type-Options',
status: 'pass',
message: 'X-Content-Type-Options properly set'
});
}
// Check Referrer-Policy
const referrerPolicy = headers['referrer-policy'];
if (!referrerPolicy) {
findings.push({
header: 'Referrer-Policy',
status: 'warn',
message: 'Missing Referrer-Policy header'
});
} else {
findings.push({
header: 'Referrer-Policy',
status: 'pass',
message: 'Referrer-Policy set'
});
}
return findings;
}
/**
* Calculate security score (0-100)
*/
private calculateScore(findings: SecurityAudit['findings']): number {
const total = findings.length;
const passed = findings.filter(f => f.status === 'pass').length;
const warned = findings.filter(f => f.status === 'warn').length;
return Math.round((passed * 100 + warned * 50) / total);
}
/**
* Generate audit report
*/
generateReport(audit: SecurityAudit): string {
const lines: string[] = [
`Security Header Audit Report`,
`URL: ${audit.url}`,
`Timestamp: ${audit.timestamp.toISOString()}`,
`Score: ${audit.score}/100`,
``,
`Findings:`
];
for (const finding of audit.findings) {
const icon = finding.status === 'pass' ? '✅' : finding.status === 'warn' ? '⚠️' : '❌';
lines.push(`${icon} ${finding.header}: ${finding.message}`);
}
return lines.join('\n');
}
}
// Usage
const validator = new SecurityHeaderValidator();
(async () => {
const audit = await validator.audit('https://your-chatgpt-app.com');
console.log(validator.generateReport(audit));
if (audit.score < 80) {
console.error('❌ Security score below threshold. Fix critical issues before deploying.');
process.exit(1);
}
})();
Run this validator in your CI/CD pipeline to prevent deploying apps with insufficient security headers.
Conclusion
Security headers are essential for protecting ChatGPT apps against XSS, clickjacking, man-in-the-middle attacks, and other client-side threats. Implement Content Security Policy with nonce-based protection, configure HSTS with preload for maximum HTTPS enforcement, enable frame protection to prevent clickjacking, and set up CORS properly to control cross-origin access. Add additional headers like X-Content-Type-Options, Referrer-Policy, and Permissions-Policy for defense-in-depth.
For comprehensive security hardening guidance, see our ChatGPT App Security Hardening Guide pillar article.
Ready to build secure ChatGPT apps without managing security headers manually? MakeAIHQ provides production-ready security configurations out of the box, including CSP, HSTS, CORS, and all protective headers. Focus on building your AI features while we handle security infrastructure. Start building with MakeAIHQ and deploy secure ChatGPT apps in minutes.
Related Resources:
- ChatGPT App Security Hardening Guide - Complete security implementation
- CORS Configuration for ChatGPT Apps - Cross-origin security
- XSS Prevention in ChatGPT Apps - Cross-site scripting defense
- Clickjacking Prevention for ChatGPT Apps - Frame-based attack protection
External Resources: