Adopting @aws-sdk/client-s3 and the other modular AWS SDK v3 clients is now a security requirement, not a preference: AWS SDK for JavaScript v2 entered maintenance mode on September 8, 2024 and reached end-of-support on September 8, 2025, after which it stopped receiving security fixes. If your Node.js services still import the monolithic aws-sdk package, you are running an unsupported credential-handling library against your cloud control plane. This guide covers the migration's security dimensions: dependency surface, credential flow, least privilege per client, and how to test without ever touching real keys.
Why the modular clients are a security win
The v2 SDK shipped every AWS service in one package. Installing it to upload a file to S3 also installed client code for dozens of services you never call. The v3 design inverts this: you install only what you use.
npm install @aws-sdk/client-s3 @aws-sdk/client-dynamodb
The security consequences are real, not cosmetic:
- Smaller dependency surface. Fewer shipped bytes means fewer places for advisories to land and less code for reviewers to reason about.
- Clearer capability inventory. Your
package.jsonnow documents which AWS services a workload talks to. When@aws-sdk/client-cognito-identity-providerappears in a service that should never touch user pools, that diff is a review conversation. - Tree-shaking. Bundlers drop unused commands, which matters in Lambda cold-start budgets and, more importantly, in keeping deployed artifacts auditable.
Treat the client list as an intent declaration and wire it into review: a new @aws-sdk/client-* dependency should trigger the same scrutiny as a new IAM permission, because that is what it foreshadows.
Credential handling: let the provider chain work
The single most common security mistake with @aws-sdk/client-s3 code is passing credentials explicitly:
// Anti-pattern: keys in config, which means keys in env files, which means keys in git
const s3 = new S3Client({
region: "ap-south-1",
credentials: { accessKeyId: process.env.AWS_KEY!, secretAccessKey: process.env.AWS_SECRET! },
});
The v3 default credential provider chain resolves credentials from the execution environment: IAM roles for Lambda and ECS, instance profiles on EC2, web identity federation on EKS, and SSO or profiles locally. The secure construction is boring:
const s3 = new S3Client({ region: "ap-south-1" });
No static keys anywhere. If you find long-lived access keys in env vars during a migration, take the opportunity to replace them with roles; our AWS IAM security best practices post covers the patterns. Rotate anything that ever landed in a .env file, because git history is forever.
Least privilege per client
Modular clients pair naturally with narrowly scoped roles. A service that imports only @aws-sdk/client-dynamodb should run under a role whose policy grants only the DynamoDB actions it issues, on the specific tables it owns. Two practices make this stick:
- Derive policies from commands. Grep the codebase for
Commandimports (PutObjectCommand,QueryCommand) and write the IAM policy from that list rather than from guesswork. The v3 command-per-operation design makes the inventory mechanical. - Fail closed on drift. When someone adds
DeleteObjectCommandand the role lackss3:DeleteObject, the error tells you the policy needs a deliberate change. That friction is the control working; resist the "just adds3:*" reflex that erases it.
S3 deserves special care: bucket policies, ACL legacy, and presigned URLs each have their own failure modes. Presigned URLs generated with @aws-sdk/s3-request-presigner inherit the signing principal's permissions, so a presigner running under an over-broad role mints over-broad URLs. Scope the signing role to s3:GetObject on the exact prefix, set short expiries, and see our S3 bucket security guide for the bucket-side controls.
Testing without credentials: npm aws-sdk-client-mock
The npm aws-sdk-client-mock package (aws-sdk-client-mock) is the community-standard way to unit test v3 code, and it has an underrated security benefit: tests that never need real credentials never leak them into CI logs or local shells.
import { mockClient } from "aws-sdk-client-mock";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
const s3Mock = mockClient(S3Client);
beforeEach(() => s3Mock.reset());
it("reads the report object", async () => {
s3Mock.on(GetObjectCommand).resolves({ /* stubbed body */ });
// exercise code under test
});
Because you mock client behavior at the command level, the mock client approach keeps tests deterministic and offline. Enforce it structurally: CI jobs that run unit tests should have no AWS credentials available at all, so an accidentally unmocked call fails loudly instead of mutating a real bucket. Reserve real-credential integration tests for a dedicated, isolated account.
Dependency hygiene for the @aws-sdk tree
The v3 SDK is a large monorepo publishing many small packages that version together. Operational guidance:
- Update as a set. Mixing
@aws-sdk/client-s3from one release wave with@aws-sdk/client-dynamodbfrom a much older one occasionally surfaces shared-internals mismatches. Group AWS SDK updates in your dependency bot config. - Watch the transitive middleware. The clients share
@smithy/*packages for HTTP handling, signing, and retries. Advisories against those affect every client at once, so your scanner needs to resolve the full tree; a composition analysis tool such as Safeguard will map one middleware advisory to every service that ships it. - Kill remaining v2 installs.
npm ls aws-sdkacross your repos finds the stragglers, including transitive ones hiding under older tooling (the pre-2021node-pre-gyplineage was a famous carrier). Post end-of-support, every remaining v2 install is unpatchable by definition.
Migration order that reduces risk
- Inventory v2 usage per service (
npm ls aws-sdkplus grep forrequire("aws-sdk")). - Migrate leaf utilities first, then request-path code; the v3 command style makes diffs reviewable.
- Replace static keys with roles as each service migrates.
- Tighten IAM to the observed command list.
- Add
aws-sdk-client-mockcoverage as you touch each module, and strip credentials from unit-test CI.
Done in that order, the migration leaves you with less code, narrower permissions, and a supported SDK, which is a rare kind of refactor.
FAQ
Is the AWS SDK for JavaScript v2 still safe to use?
No. It entered maintenance mode on September 8, 2024 and reached end-of-support on September 8, 2025, so it no longer receives security updates. Migrate remaining workloads to the modular v3 clients such as @aws-sdk/client-s3.
Should I pass credentials to @aws-sdk/client-s3 in code?
Almost never. Construct clients with only a region and let the default provider chain resolve credentials from IAM roles, instance profiles, or SSO. Explicit key material in config is how keys end up in env files and git history.
What is aws-sdk-client-mock for?
It stubs v3 clients at the command level so unit tests run offline with zero AWS credentials. Pair it with credential-free CI for unit tests, so any unmocked call fails instead of hitting real infrastructure.
Do the modular clients reduce vulnerabilities?
They reduce exposure: you ship only the service clients you use, so fewer advisories apply to you and the ones that do are easier to trace. Shared @smithy/* middleware still needs full-tree scanning since one advisory there touches every client.