Frontend Engineering/
Lesson

You asked AI to add Stripe payments to your app. The generated code imported the Stripe SDKWhat is sdk?A pre-built library from a service provider that wraps their API into convenient functions you call in your code instead of writing raw HTTP requests. and initialized it with your secret key, right in the React component, shipped to every browser that loads the page. Or you asked for a "production-ready Express server" and got no security headers at all, no CSP, no HSTSWhat is hsts?HTTP Strict Transport Security - a response header that tells browsers to only connect to your site over HTTPS for a specified duration., no frame protection. AI treats security headers as optional extras and routinely puts secrets where they don't belong. Both of these defaults ship vulnerabilities.

Client-side secrets: the silent leak

When AI generates code that calls third-party APIs, it almost always puts the APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. key right next to the fetch call. If that code runs in the browser, the key is visible to anyone who opens DevTools.

// AI generates this for a React component
const OPENAI_KEY = 'sk-abc123...';  // visible in browser source

async function askAI(question) {
  const res = await fetch('https://api.openai.com/v1/chat/completions', {
    headers: { 'Authorization': `Bearer ${OPENAI_KEY}` },
    // ...
  });
}

This is not obscured by building or bundling. The key ends up in the JavaScript bundleWhat is bundle?A single JavaScript file (or set of files) that a build tool creates by combining all your source code and its imports together. that browsers download, and anyone can extract it.

Where AI puts secretsWhat actually happensWhat you should do
In React/frontend codeVisible in browser DevTools, bundleMove to server-side API route
In .env without .gitignoreCommitted to Git, visible in repoAdd .env to .gitignore before first commit
Hardcoded in server codeExposed if source code leaksUse environment variables
In next.config.js public varsBundled into client-side codeUse server-only env vars (no NEXT_PUBLIC_ prefix)
AI pitfall
When you ask AI to integrate any third-party API (Stripe, OpenAI, SendGrid, Firebase), it puts the API key directly in the file where it's used. If that file is a React component or any client-side code, the key ships to every user's browser. Always route secret-requiring API calls through your own backend.

The fix is a proxy pattern: your frontend calls your own backend, and your backend calls the third-party API with the secret key:

// Frontend - no secrets here
const res = await fetch('/api/ask-ai', {
  method: 'POST',
  body: JSON.stringify({ question })
});

// Backend - secret stays server-side
app.post('/api/ask-ai', async (req, res) => {
  const response = await fetch('https://api.openai.com/v1/chat/completions', {
    headers: { 'Authorization': `Bearer ${process.env.OPENAI_KEY}` },
    // ...
  });
  const data = await response.json();
  res.json(data);
});
AI pitfall
AI sometimes uses NEXT_PUBLIC_ or VITE_ prefixed environment variables for secret keys, thinking it's "using env vars properly." These prefixes specifically mean the variable is bundled into client-side code. For secrets, use non-prefixed env vars that are only available server-side, and access them through API routes or server-side rendering functions.
02

Why security headers matter

Think of security headers as instructions you give to browsers. Without them, browsers make their own decisions about what to allow, and those defaults prioritize compatibility over security. With the right headers, you explicitly tell the browser: "Only load scripts from these domains. Never display this page inside an iframe. Always use HTTPSWhat is https?HTTP with encryption added, so data traveling between your browser and a server can't be read or tampered with by anyone in between.."

The practical benefit is defense in depth. Even if an attacker finds an XSSWhat is xss?Cross-Site Scripting - an attack where malicious JavaScript is injected into a web page and runs in other users' browsers, stealing data or hijacking sessions. vulnerability in your code, a strong Content Security PolicyWhat is content security policy?An HTTP header that tells the browser which sources of scripts, styles, and other resources are allowed to load on your page, blocking unauthorized code. might prevent their injected script from executing.

03

Content Security PolicyWhat is content security policy?An HTTP header that tells the browser which sources of scripts, styles, and other resources are allowed to load on your page, blocking unauthorized code.

CSP is the most powerful and complex security header. It defines a whitelist of sources from which the browser is allowed to load resources:

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com

This tells the browser: load everything from the same originWhat is origin?The combination of protocol, domain, and port that defines a security boundary in the browser, like https://example.com:443. by default, and also allow scripts from cdn.example.com. Any script from anywhere else, including inline <script> tags, is blocked.

The key directives you'll configure most often:

DirectiveControlsExample value
default-srcFallback for all resource types'self'
script-srcJavaScript sources'self' https://cdn.example.com
style-srcCSS sources'self' 'unsafe-inline'
img-srcImage sources'self' data: https:
connect-srcfetch/XHR destinations'self' https://api.example.com
frame-srcIframe sources'none'
object-srcFlash and plugins'none' (always disable)
'unsafe-inline' in script-src allows inline <script> tags and event handlers. Avoid it if possible, it significantly weakens XSS protection. Modern approaches use nonces instead.

Testing CSP without breaking your site

CSP misconfigurations break real functionality. Test with Content-Security-Policy-Report-Only first, it logs violations without actually blocking anything:

res.setHeader(
  'Content-Security-Policy-Report-Only',
  "default-src 'self'; script-src 'self'; report-uri /csp-report"
);
04

Other essential headers

HeaderProtects againstRecommended value
Strict-Transport-SecuritySSL stripping, downgrade attacksmax-age=31536000; includeSubDomains
X-Frame-OptionsClickjackingDENY
X-Content-Type-OptionsMIME sniffing attacksnosniff
Referrer-PolicySensitive URL leakagestrict-origin-when-cross-origin
Permissions-PolicyFeature abuse after XSSDisable unused APIs with ()

HSTSWhat is hsts?HTTP Strict Transport Security - a response header that tells browsers to only connect to your site over HTTPS for a specified duration.

HSTS forces browsers to always use HTTPSWhat is https?HTTP with encryption added, so data traveling between your browser and a server can't be read or tampered with by anyone in between.. Once a browser receives this header, it converts all HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. requests to HTTPS internally, the insecure request never leaves the browser:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Only send HSTS over HTTPS, and only when you're confident your entire site works correctly over HTTPS. Getting locked out of HTTP before you're ready is painful to recover from.

X-Frame-Options

ClickjackingWhat is clickjacking?An attack where a malicious site embeds your page in a hidden iframe and tricks users into clicking elements on it unknowingly. is an attack where a malicious site embeds your application in a transparent iframe and overlays invisible buttons that trick users into clicking things in your app. X-Frame-Options: DENY stops it.

05

Implementing with helmetWhat is helmet?An Express middleware package that sets recommended HTTP security headers (CSP, HSTS, X-Frame-Options) in one line..js

Configuring all these headers manually is tedious and error-prone. helmet.js is the standard solution for Express:

const helmet = require('helmet');

// One line - sensible defaults for all headers
app.use(helmet());

// Customized for production
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "https://cdn.example.com"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "https://api.example.com"],
      objectSrc: ["'none'"],
      frameAncestors: ["'none'"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  },
  frameguard: { action: 'deny' },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));
AI pitfall
When you ask AI for an Express server setup, it almost never includes helmet.js or any security headers. The generated server will have CORS configured (because you'll get errors without it) but zero security headers. Always add app.use(helmet()) as one of the first middleware lines in any Express app.
06

Quick security checklist

CheckHow to verify
No secrets in frontend codeSearch codebase for API key patterns (sk-, key=, hardcoded tokens)
.env in .gitignoreRun git log --all -- .env, should return nothing
Security headers presentRun your domain through securityheaders.com
CSP configuredCheck response headers in DevTools Network tab
HTTPS enforcedHSTS header present with long max-age
javascript
// Security headers + secrets management

const express = require('express');
const helmet = require('helmet');
const app = express();

// Set all security headers
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "https://cdn.jsdelivr.net"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "https://api.myapp.com"],
      objectSrc: ["'none'"],
      frameAncestors: ["'none'"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  },
  frameguard: { action: 'deny' },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));

// Proxy pattern: frontend never sees the secret
app.post('/api/ask-ai', async (req, res) => {
  const response = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.OPENAI_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: 'gpt-4',
      messages: [{ role: 'user', content: req.body.question }]
    })
  });
  const data = await response.json();
  res.json(data);
});