Safeguard
Security

Dependency Injection in JS: A Practical Security Guide

Dependency injection in JS improves testability, but it also becomes an attack surface when injection is dynamic or unvalidated. Here is how to keep it safe.

Karan Patel
Platform Engineer
6 min read

Dependency injection in JS is a design pattern where a component receives its dependencies from the outside instead of constructing them itself, and while it makes code more testable and modular, it becomes a security concern the moment what gets injected is chosen at runtime from data you do not fully control. That is the tension this guide works through: dependency injection JS patterns are good engineering, but "inject whatever this string names" is a well-worn path to remote code execution.

Most JS dependency injection is completely benign. The risk is narrow and specific, and once you can spot it, you can keep the pattern's benefits without the exposure.

What dependency injection in JS looks like

At its simplest, dependency injection in JS means passing collaborators as arguments rather than importing or new-ing them inside a function.

// Without DI: the function hard-wires its dependency
function createUser(data) {
  const db = require('./db');
  return db.insert('users', data);
}

// With DI: the dependency is passed in
function createUser(db, data) {
  return db.insert('users', data);
}

The second version is easier to test because you can pass a fake db, and easier to reconfigure because the wiring lives in one place. Frameworks like NestJS formalize this with decorators and an injector, Angular has a full DI container, and libraries such as InversifyJS or Awilix bring container-based DI to plain Node. All of these are legitimate and, used normally, carry no special risk.

Where dependency injection JS turns into an attack surface

The danger appears when the thing being injected is selected dynamically from untrusted input. The classic shape is a "plugin loader" or "strategy selector" driven by a request:

// Dangerous: attacker controls what module gets loaded
app.post('/run/:handler', (req, res) => {
  const handler = require(req.params.handler);   // arbitrary module load
  res.json(handler.run(req.body));
});

Here the require argument comes straight from the URL. An attacker can point it at unexpected modules, path-traverse the filesystem, or reach built-ins to pivot toward command execution. This is a form of injection in JS that has nothing to do with SQL or HTML; it is code being resolved and executed based on attacker data.

A related pattern is resolving a service by name from a container using untrusted keys:

// Risky: user decides which service to instantiate
const service = container.resolve(req.query.service);

If the container can construct anything registered in it, and some of those things have dangerous side effects, you have handed the caller a menu.

The worst version combines DI with dynamic evaluation. Any design where a dependency is a string of code that later reaches eval, Function(), or vm.runInContext with user input is a straightforward injection JS vulnerability. The pattern turns "flexible" into "arbitrary" for whoever can reach the input.

The npm dimension

Dependency injection in the broadest sense also describes what your package manager does: it injects third-party code into your application at install time. That is worth naming, because the trust you extend to an injected module at runtime is the same trust you extend to every transitive dependency in node_modules.

A DI container does not make a malicious or vulnerable package safe. If you register a compromised module as a provider, DI just makes it easy to distribute that instance across your app. So securing dependency injection in JS has two layers: the runtime pattern, and the supply chain feeding it. Continuous software composition analysis covers the second, flagging when an injected package carries a known advisory. An SCA tool such as Safeguard can trace that back through the dependency graph so you know which provider to swap.

Secure patterns for JS dependency injection

You keep the benefits and drop the risk by making injection static and explicit wherever it touches untrusted input.

Wire dependencies at startup, not from requests. Build your object graph once, from configuration you control, before any request arrives. Requests should select behavior from a fixed, small set, never name arbitrary modules.

Use an allowlist for dynamic selection. If you genuinely need runtime choice, map validated keys to known implementations:

const handlers = {
  export: exportHandler,
  import: importHandler,
};

app.post('/run/:handler', (req, res) => {
  const handler = handlers[req.params.handler];
  if (!handler) return res.status(400).json({ error: 'unknown handler' });
  res.json(handler.run(req.body));
});

The object literal is the entire security boundary. Nothing outside those keys can be reached, and req.params.handler can no longer influence require.

Never pass user input to require, import(), eval, or Function(). If you find one of these taking a variable that traces back to a request, treat it as a finding, not a style preference.

Prefer constructor injection over service-locator lookups by string. Passing concrete dependencies as arguments is easier to audit than resolving them by name from a global container, because the wiring is visible at the call site.

Validate and type your configuration. DI wiring often reads from environment variables or config files. Validate those at boot so a misconfigured or tampered value fails loudly instead of silently injecting the wrong thing.

Reviewing it in practice

When you review code that uses dependency injection in JS, ask three questions. Where does each dependency come from? Can any of those origins be influenced by an incoming request? Does resolution ever pass through require, import(), or an evaluator with a dynamic argument? Clean answers to those three cover the vast majority of real risk. If you want a structured walkthrough of secure-coding patterns like this, the Safeguard Academy has practitioner-focused material.

FAQ

Is dependency injection in JS a security risk by itself?

No. Dependency injection is a neutral design pattern that improves testability and modularity. It only becomes a security risk when the dependency being injected is chosen dynamically from untrusted input, especially if resolution passes through require, import(), eval, or Function().

What is the most dangerous dependency injection JS pattern?

Passing user-controlled data to a module loader or evaluator, for example require(req.params.name) or Function(userInput). This lets an attacker influence which code executes and can lead to remote code execution. Use a fixed allowlist mapping validated keys to known implementations instead.

How is npm related to dependency injection?

Installing packages injects third-party code into your app, and a DI container will happily distribute a compromised or vulnerable package throughout your codebase. Securing JS dependency injection therefore includes supply-chain hygiene: monitor your dependencies with software composition analysis and pin versions.

Should I use a DI framework like NestJS or InversifyJS?

They are fine and widely used. The framework is not the risk. What matters is that you wire dependencies at startup from configuration you control and never let request data decide which arbitrary module or service gets loaded.

Never miss an update

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