Safeguard
Supply Chain

copy-webpack-plugin and terser-webpack-plugin: Build Pipeline Hygiene

The copy-webpack-plugin npm package and terser-webpack-plugin sit in almost every webpack build. Here's how to configure both without leaking files or shipping stale minifiers.

Marcus Chen
DevSecOps Engineer
6 min read

The copy-webpack-plugin npm package and terser-webpack-plugin are the two build-pipeline dependencies most teams configure once and never audit again — and both have a concrete failure mode: one can silently copy files you never meant to ship, the other has a real CVE history in its underlying minifier. Neither plugin is dangerous by design. Both become risky through the default-and-forget pattern: a glob that's slightly too wide, a transitive terser version that's slightly too old. This guide covers what each plugin actually does, the specific misconfigurations worth checking today, and how to keep both patched without babysitting them.

What copy-webpack-plugin does, and the glob trap

copy-webpack-plugin copies individual files or whole directories into the build output directory. That's it — no transformation, no bundling, just file transport driven by glob patterns:

const CopyPlugin = require("copy-webpack-plugin");

module.exports = {
  plugins: [
    new CopyPlugin({
      patterns: [
        { from: "public/assets", to: "assets" },
      ],
    }),
  ],
};

The trap is pattern breadth. A from: "public" or, worse, from: "." with a permissive glob will happily transport .env.local, config/credentials.json, editor backup files, or a stray database dump sitting in the source directory straight into dist/ — which then gets uploaded to a CDN with public read access. This is not hypothetical; "secrets in the static bucket" incidents very often trace back to an over-broad copy step rather than a deliberate upload.

Three habits close the gap:

  1. Copy from dedicated asset directories only (public/assets, static/), never from the project root.
  2. Use globOptions.ignore defensively even inside those directories: ignore: ["**/*.map", "**/.*"] keeps dotfiles and source maps out.
  3. Add a post-build check in CI that fails the pipeline if dist/ contains anything matching a denylist (*.env*, *.pem, *credentials*). It's a five-line script and it has saved real teams real embarrassment.

If you install it fresh, note that on npm copy-webpack-plugin tracks webpack major versions closely — v11 and v12 both require webpack 5, and the maintainers drop old Node versions aggressively, so the version your three-year-old boilerplate pinned is probably several majors behind.

What terser-webpack-plugin does, and why you rarely install it

terser-webpack-plugin wires the Terser minifier into webpack's optimization phase. If you're on webpack 5, you already have it: webpack 5 ships terser-webpack-plugin out of the box and uses it for production builds without any configuration. You only add it to package.json yourself when you want to customize minifier options:

const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: { drop_console: true },
          format: { comments: false },
        },
      }),
    ],
  },
};

That "you already have it" detail is the hygiene point. Because the plugin — and Terser itself — arrive transitively, teams often don't realize which Terser version their build runs. Searching npm terser-webpack-plugin and installing the latest gets you a current Terser, but a pinned webpack from 2021 in a legacy repo can carry the vulnerable range below.

The Terser CVE you should actually check for

CVE-2022-25858: Terser before 4.8.1, and 5.0.0 up to but not including 5.14.2, is vulnerable to regular expression denial of service. Insecure regex usage inside the minifier meant that specially crafted input being minified could trigger catastrophic backtracking and hang the process. The fix (4.8.1 / 5.14.2) added a regexp_is_safe validation step.

A minifier ReDoS sounds low-stakes — it runs at build time, not in production. But think about where your builds run: CI runners that also hold deploy credentials, and dev machines. Any workflow that minifies third-party or user-influenced JavaScript (plugin systems, user-supplied widget code, monorepos that build vendored snippets) turns a build-time hang into a denial-of-service lever, and CI time-outs that mask a hang are a miserable thing to debug.

Checking is one command:

npm ls terser

If anything in the tree resolves below 4.8.1 or in the 5.0.0–5.14.1 range, add an override:

{
  "overrides": {
    "terser": "^5.14.2"
  }
}

Transitive pins like this are exactly the class of issue that version-range scanning catches and humans don't — an SCA tool such as Safeguard will flag the vulnerable terser resolution in your lockfile even when your own package.json never mentions terser at all.

Keeping both plugins current without churn

Build tooling updates are low-risk compared to runtime dependencies — there's no API surface your production code calls — so the cost/benefit favors staying current:

  • Track the webpack major you're on, and take copy-webpack-plugin/terser-webpack-plugin majors that correspond to it. Both projects document the mapping in their READMEs.
  • Let Renovate or Dependabot group "webpack tooling" into a single weekly PR. One review, one CI run.
  • After each bump, diff the output: ls -R dist/ before and after catches a copy-pattern behavior change; comparing bundle sizes catches a minifier regression. Our webpack-bundle-analyzer guide covers making that size diff systematic.

Build plugins are supply-chain surface too

A final framing worth internalizing: everything in devDependencies executes with full permissions on your build machine. copy-webpack-plugin has filesystem access by definition; terser-webpack-plugin executes as part of every production build. A compromised version of either — or of their own dependencies — runs inside CI with whatever tokens the job carries. The 2021–2025 wave of npm maintainer-account compromises hit build tooling as readily as runtime libraries, so the same controls apply: lockfile-enforced installs (npm ci), install scripts disabled where possible, and provenance checks on version bumps rather than blind auto-merge.

FAQ

Do I need terser-webpack-plugin in my package.json on webpack 5?

Only if you customize minification options (dropping consoles, tweaking compress passes, swapping in esbuild or SWC as the minifier). For default production minification, webpack 5 already includes and uses it.

Can copy-webpack-plugin transform files while copying?

It supports a transform function per pattern for small tweaks (e.g., injecting a build hash into a manifest), but heavy transformation belongs in loaders. If you find yourself writing significant logic in transform, that asset probably wants to be a real module in the build graph.

How do I know which Terser version my build uses?

Run npm ls terser (or yarn why terser / pnpm why terser). The resolved version comes from terser-webpack-plugin's dependency range and your lockfile, not from anything you pinned directly — which is why it drifts unnoticed.

Is minified code a security control?

No. Minification shrinks and mangles code for performance; it is trivially reversible enough that it provides no confidentiality. Never rely on it to hide endpoints, keys, or logic — and never put secrets in client bundles at all.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.