Dependency injection in JavaScript is a pattern for supplying a module's collaborators, its database client, logger, HTTP client, config object, from the outside rather than having the module construct them itself. That indirection is good for testability and good for swapping implementations, but it also changes where trust boundaries live in your codebase, and teams that adopt DI purely for architecture reasons often don't think through the security side until something goes wrong. Most tutorials on dependency injection JS patterns focus purely on testability and wiring, which is exactly why the security gap below gets missed.
This post is not an argument against dependency injection. It's a set of things worth checking if your codebase uses it, whether through a formal container like InversifyJS or Awilix, or through the more common JavaScript pattern of passing dependencies as constructor or factory arguments.
What does dependency injection actually look like in JavaScript?
Most JavaScript dependency injection is informal. Instead of a class reaching into a global require or import to get its database client, the client is passed in through a constructor or a factory function:
function createUserService({ db, logger, mailer }) {
return {
async createUser(input) {
const user = await db.users.insert(input);
logger.info("user created", user.id);
await mailer.send(user.email, "welcome");
return user;
},
};
}
Frameworks like NestJS and Angular formalize this with decorators and a container that resolves the dependency graph automatically. Either way, the core idea is the same: the module declares what it needs, and something else decides what it gets.
Why does this matter for security review?
It matters because the security properties of a module are no longer fully determined by reading that module's file. If createUserService gets a mailer that's actually a test double, or a db client scoped to the wrong tenant, the vulnerability isn't visible in the function body at all, it's in the wiring code that assembles the container. Code reviewers who read modules in isolation, which is how most PR review works, can miss the fact that a dependency was swapped for something with weaker guarantees.
This is a bigger deal in multi-tenant systems. A common bug pattern is a DI container that resolves a "current tenant" scoped dependency, like a database connection or an authorization checker, at the wrong lifetime, singleton instead of request-scoped, so that tenant A's request ends up reusing tenant B's resolved dependency under load. This class of bug is subtle enough that unit tests rarely catch it; it shows up in production under concurrency.
What are the concrete risks to check for?
A few patterns come up repeatedly in real codebases:
Config and secrets injected too broadly. If your DI container injects a full config object into every service because it's convenient, you've made every module a potential source of secret leakage, logging the config object anywhere, even in error output, exposes credentials. Inject the narrowest slice of config a module actually needs.
Mocking that leaks into production. Test doubles and fakes are usually wired through the same DI mechanism as real dependencies. A misconfigured environment check, or a build that doesn't strip test wiring, can leave a fake auth checker or a permissive rate limiter active in a deployed environment. This has caused real incidents where a "development mode" flag defaulted to on.
Singleton lifetime for tenant-scoped state. As mentioned above, if a container is misconfigured to cache a dependency that should be resolved per-request, you get cross-tenant data bleed. Review container configuration for lifetime annotations (transient, scoped, singleton) as carefully as you'd review an access-control check.
Dynamic resolution from untrusted input. Some DI setups resolve a dependency by a string key that ends up derived from user input, a plugin name, a webhook type. If that string isn't validated against an allowlist, you have a code-execution-adjacent vulnerability: an attacker who can influence the resolution key can potentially get the container to instantiate something unintended.
How should you scan and test DI-heavy code?
Static analysis tools that follow data flow through function calls generally struggle with DI containers, because the actual dependency graph is only known at runtime, resolved by a container library, not by static import statements. This is one of the places where SAST tools need language- and framework-specific support for popular DI containers to trace calls correctly; a naive scanner will simply lose the trail at the container boundary and under-report.
Practically, that means: test your wiring code directly, not just your business logic. Write integration tests that assert the container resolves the right scope for tenant-scoped dependencies. And if you're evaluating a static analysis tool, ask specifically how it handles common DI frameworks in your stack, because the answer varies a lot between vendors.
FAQ
Is dependency injection itself a security risk?
No. DI is a design pattern, not a vulnerability class. The risk comes from misconfigured wiring: wrong lifetimes, overly broad injected objects, or dynamic resolution from untrusted input. Reviewed carefully, DI code is no riskier than any other composition pattern.
Does dependency injection make static analysis harder?
Often, yes. Because the concrete implementation behind an interface is decided by container configuration rather than a static import, some scanners lose the call graph at that boundary. Framework-aware scanners handle popular DI libraries explicitly; check before you buy.
What's the single highest-value thing to review in a DI container?
Lifetime scoping for anything tenant- or request-specific. Singleton-scoped dependencies that should be request-scoped are the most common source of real incidents in DI-heavy multi-tenant applications.
Do NestJS or Angular DI containers have different risks than manual JavaScript DI?
The risks are the same in kind, wrong lifetimes, leaked config, dynamic resolution, but formal containers make misconfiguration easier to spot in a review because lifetimes and providers are declared explicitly, versus hand-rolled DI where wiring logic is scattered across the codebase.
Is "dependency injection JS" the same thing as a DI framework?
Not necessarily. Most JavaScript dependency injection in real codebases is informal, plain constructor or factory arguments, with no framework at all. A formal container like InversifyJS, Awilix, NestJS, or Angular's DI system adds explicit lifetime and provider declarations on top of the same underlying idea, which is why framework-based DI is usually easier to audit for the risks described above.