# Add Security Headers to a Node.js App

Yes. An agent adds helmet with a content security policy fitted to your templates, hardens the session cookie, checks every page still works and opens a PR.

[Run this on Rush](https://cloud.getrush.ai/run/add-security-headers/nodejs/), in a Project that has your repository.

## What the agent does

The agent clones the app into its own cloud sandbox and reads how responses are built before it adds a header. In an Express app like OWASP/NodeGoat, `server.js` requires `express`, `express-session`, `consolidate` with `swig` templates and the MongoDB driver, and the lines that would harden it are already there as comments: `// const helmet = require("helmet")`, and a block marked "Fix for A5 - Security MisConfig" that would call `app.disable("x-powered-by")`, `helmet.frameguard()`, `helmet.noCache()`, `helmet.contentSecurityPolicy()`, `helmet.hsts()` and `nosniff()`. `package.json` already lists `helmet` at `^2.0.0` and `dont-sniff-mimetype`, versions old enough that the middleware names in the comment no longer match a current release, so the first change is the dependency. The session is set up with `saveUninitialized: true`, `resave: true`, a `cookieSecret` from `config/env/all.js`, and no `cookie` options; the `httpOnly` and `secure` settings are in another comment. The server is plain `http.createServer(app)` on port 4000.

A content security policy is only as good as the inventory behind it, so the agent reads the templates. `app/views/layout.html` loads `/vendor/jquery.min.js`, `/vendor/bootstrap/bootstrap.js`, `/vendor/bootstrap/bootstrap-tour.js`, the Raphael and Morris chart libraries, `/js/chart/chart-data-morris.js` and `/js/tour/redirects-steps.js`, all from `app/assets` served by `express.static`, and then loops over `environmentalScripts`; `config/env/development.js` fills that list with a livereload script fetched from port 35729, while `config/env/production.js` exports an empty object. `app/views/login.html` carries one inline `<script type="application/javascript">` of its own. From that the agent writes a policy with `default-src 'self'`, a `script-src` of `'self'` plus a per-request nonce that the layout and login templates print on their inline script tags, `style-src 'self'`, `img-src 'self' data:`, `frame-ancestors 'none'`, `form-action 'self'` and `object-src 'none'`, and in development only it adds the livereload origin to `script-src` and `connect-src`.

The other headers follow the comment but with current defaults: `helmet()` for `X-Content-Type-Options`, `Referrer-Policy`, `X-Frame-Options` and the rest, `app.disable("x-powered-by")`, and HSTS applied only when `NODE_ENV` is `production`, because the server itself speaks HTTP and a `Strict-Transport-Security` header on a plain port 4000 would be wrong in development and a trap behind a proxy that has not been told about with `app.set("trust proxy", 1)`. The session gets `cookie: { httpOnly: true, sameSite: "lax", secure: production }` and a name that is not the library default. The agent leaves the deliberately vulnerable tutorial routes alone; the job is headers and the cookie, and it says so in the pull request.

Then it checks that nothing it shipped breaks a page. `npm test` runs `grunt test`, which is `mochaTest:unit` over `test/unit/*.js`, and that directory does not exist in the repository, so the agent adds `test/unit/headers.js`, a Mocha test that starts the app against the sandbox's MongoDB and asserts the headers on `/`, `/login` and a signed-in `/dashboard`, which gives `npm test` something to run. The real check is the browser suite: `npm run test:ci` runs the Cypress specs under `test/e2e/integration/`, from `login_spec.js` to `tutorial_spec.js`, and a policy that blocks the login page's inline script or a chart library shows up there as a failed spec. It seeds the database with `npm run db:seed` first, as `README.md` says, and runs `grunt precommit` for `jsbeautifier` and `jshint` under the `.jshintrc` rules (double quotes, four-space indent).

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Add security headers to this Express app. Read server.js, package.json, config/config.js, config/env/all.js, config/env/development.js, config/env/production.js, app/views/layout.html, app/views/login.html and app/views/signup.html, and list every script and style source the templates load, including the environmentalScripts loop and any inline script. Upgrade helmet to a current release, enable it with a content security policy built from that inventory, using a per-request nonce for the inline scripts and allowing the livereload origin only outside production, disable x-powered-by, send HSTS only in production behind trust proxy, and set httpOnly, sameSite and a production-only secure flag on the session cookie with a non-default name. Start MongoDB in the sandbox, run npm run db:seed and npm start, and curl -I the home, login and dashboard pages to show the headers. Add a Mocha test under test/unit/ that asserts them so npm test runs something, run npm run test:ci so the Cypress specs prove every page still loads under the policy, run grunt precommit, and open a pull request that lists each header with its value and the pages checked. Do not change the tutorial routes or their intended vulnerabilities.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch that changes `server.js` and `package.json` with `package-lock.json`, adds a nonce attribute to the inline script tags in `app/views/layout.html` and `app/views/login.html`, and adds `test/unit/headers.js`. The description holds a table of the response headers on `/`, `/login` and `/dashboard` as `curl -I` printed them, the content security policy with a line per directive and the template line that justifies each source, and the Cypress summary.

The transcript at `GET /o/{org}/sessions/{id}/messages` keeps the MongoDB start, the seed, every `curl`, the Mocha run and the full Cypress output, so a reviewer can see which spec exercised which page under the new policy. `GET /o/{org}/p/{project}/sessions/{id}/changes` shows the diff.

Before merging, a reviewer confirms the production branch of the policy has no livereload origin, that HSTS is gated on `NODE_ENV` and the proxy setting matches the deployment, and that the session cookie name change does not sign everyone out at a bad moment. A follow-up on the same session can tighten `style-src` once the inline styles in the templates are moved to files.

## Run it on every Express service without helmet

Run it once per service from the terminal, then let a script do the rest. A loop on your server lists the repositories whose `package.json` lacks `helmet`, sends one `POST /o/{org}/sessions` per repository with this body and that repository in `resources`, and uses the repository name as the `request_id` so a rerun returns the run already started. Each run gets a sandbox of its own with its own MongoDB.

Give the key a webhook URL and Rush POSTs `run.completed` with `data.session_url` when each turn ends. A handler reads the session, pulls the pull request link and the header table from the result, and posts them where the service owners will see them; a run that ended `needs_review` because a Cypress spec failed under the policy is listed for a person to read.

## Why run it on Rush?

A content security policy written from memory breaks the first page with an inline script, and the only way to know is to load every page with the policy on. In its own sandbox the agent runs the app against a real MongoDB, drives the Cypress suite the repository already has, and keeps the output, so the pull request shows which pages were exercised rather than asserting that the policy is safe.

You start it from a terminal and come back to a pull request. The sandbox parks when the turn ends and wakes for a follow-up, so moving the inline styles out of the templates and tightening `style-src` is the next message on the same session, with the same database and seed.

## Frequently asked questions

### Will the policy break the login page?

`app/views/login.html` has an inline script, so a policy with `script-src 'self'` alone would block it. The agent adds a per-request nonce that the template prints on that tag and the policy allows, then runs `login_spec.js` and the other Cypress specs to show the page still works. Any spec that fails under the policy ends the run `needs_review`.

### Why is HSTS only sent in production?

The app listens with `http.createServer` on port 4000, so there is no TLS to pin in development, and a `Strict-Transport-Security` header sent through a proxy that terminates TLS needs `trust proxy` set for the `secure` cookie flag to work. The agent gates both on `NODE_ENV` and documents the proxy setting in the pull request.

### Does it fix the other vulnerabilities in the app?

No. The prompt limits the job to response headers and the session cookie, and the pull request says the tutorial routes were left as they are. Escaping, CSRF tokens and the rest of the commented fixes in `server.js` are separate changes with their own tests.

### Why add a test under test/unit?

Because `npm test` already points there: `Gruntfile.js` runs `mochaTest:unit` over `test/unit/*.js`, and the directory does not exist, so the script passes while running nothing. A small Mocha test that asserts the headers on three pages makes the command meaningful and guards the policy against a later edit.
