+1 (405) 383-8943

Passing a security review with a ten-year-old Ember app

A fair number of the upgrade projects we get called into do not start with an engineer. They start with a customer's security questionnaire, an enterprise procurement review, or an internal audit that produced a spreadsheet of findings against an app nobody had examined in four years. The framework is not the finding. The findings are an end-of-life Node version, several hundred lines of npm audit output, a jQuery copy from 2017, and an empty answer in the box marked "Content Security Policy."

This work is worth doing on its own merits, but it is also the single most common reason a parked app finally gets budget. So it is worth knowing what a review actually turns up in an Ember codebase, which findings are real, and what order to fix them in.

Separate the four kinds of finding first

Before committing to anything, sort the spreadsheet. Nearly every finding in a mature Ember app falls into one of four buckets, and they have very different costs.

Build-time only. A vulnerable package that exists solely in the Broccoli pipeline, a codemod, or a test helper. It never ships to a browser. It is still worth cleaning up, because the audit tool will keep reporting it and because your CI machine runs that code — but it is not an application vulnerability, and saying so precisely is part of the job.

Shipped and reachable. A dependency that ends up in vendor.js or app.js and handles untrusted input: a markdown renderer, a sanitizer, a date parser, an old jQuery. These are the real ones.

Shipped and inert. Something in the bundle that is technically vulnerable but not exercised by any path in your app. Real enough to fix, low enough priority to schedule.

Platform and process. Node version, CI runner image, how sourcemaps are published, whether dependencies are pinned, who can publish to your private registry. Usually the fastest wins in the pile.

The deliverable from this triage is a short written answer per finding, not a green audit run. Reviewers accept "build-time only, not shipped, evidence attached" far more often than teams expect. Chasing a zero-finding npm audit in a ten-year-old app is a way to spend a quarter and still fail the next review.

Find out what is actually in the bundle

The honest way to answer "is this shipped?" is to look. Build production and read the output — with Embroider or Vite you get a normal bundle you can run through a standard analyzer; on a classic build, ember-cli-bundle-analyzer or a sourcemap explorer will do. What you want is the list of third-party modules present at runtime, which is almost always shorter and stranger than package.json suggests. Expect to find two date libraries, a copy of lodash pulled in by one addon, and something in vendor/ that was committed by hand years ago and is not in any manifest at all.

That last category is the one audit tooling cannot see. Hand-vendored files under vendor/ or public/ predate most teams' dependency discipline and are invisible to npm audit, Dependabot, and every SCA scanner the reviewer is running. Inventory them explicitly.

Node and CI are usually the cheapest real fix

A surprising share of review failures are not about the app at all. The app builds on Node 14 because an old node-sass or a Broccoli plugin with a native binding refuses to compile on anything newer, and the CI image was pinned to match and then forgotten. End-of-life Node is an unambiguous finding, easy for a reviewer to verify, and awkward to argue with.

It is also usually a one-to-two-week job rather than a project: move off native-binding packages (node-sass to sass is the classic), bump ember-cli and broccoli enough to support current Node, update the CI image, and pin the runtime in package.json engines so it does not drift back. Do this before the dependency work. Half the packages you want to upgrade will not install cleanly on the old runtime anyway.

jQuery, and the rest of the actually-shipped list

If your app predates Ember 3.4, there is a good chance jQuery is still in the bundle — sometimes because @ember/jquery is still installed, sometimes because a single addon depends on it, occasionally because four lines of $.ajax in an initializer never got rewritten. Scanners flag it reliably. The fix is rarely hard and is almost always a matter of finding the last few call sites: this.element.querySelector for the DOM work, fetch for the requests, and then removing the integration entirely so the payload drops too.

The same pattern applies to the rest of the shipped-and-reachable list. Most are single-dependency swaps with a small blast radius. Treat them as ordinary upgrade work on the main branch, one PR each, not as a security sprint.

Content Security Policy is a question you will be asked

Ember apps can run under a reasonably strict CSP, but a mature one often cannot without work: inline styles from old addons, eval reachable through a template compiler shipped to production, a third-party script tag added for analytics in 2018. Start in report-only mode with ember-cli-content-security-policy, collect violations from real traffic for a couple of weeks, then fix them in order of frequency. Enforce only when the report stream is quiet. Going straight to enforcement on a decade-old app breaks something visible within the hour.

While you are there: check that production sourcemaps are not published to the public origin unless you intend them to be, that the template compiler is not being shipped, and that your error reporter is not attaching full request bodies.

Then make it stay fixed

The review comes back next year. The difference between a two-week refresh and another three-month project is whether anything changed in between: automated dependency PRs that a human actually merges, an audit step in CI that fails on shipped-and-reachable findings only, a pinned Node version, and the LTS cadence running on schedule. Security currency and upgrade currency are the same habit viewed from two angles, which is the useful thing to tell whoever is funding this.

Where this sits relative to the bigger work

A security review is a good forcing function and a poor plan. It will get you a current Node, a cleaner dependency graph, and a CSP — all prerequisites for an LTS hop, an Embroider or Vite build, and an Octane conversion, and all worth having regardless. It will not, by itself, make the app easier to hire for or cheaper to change.

So take the budget, sequence the findings honestly, and put the remaining modernization plan in the same document. The reviewer's spreadsheet is the best opening anyone on your team is going to get this year for a conversation about the rest of it.