Secure by Design
Secure by design means building software whose architecture and defaults carry the security burden, with risky capabilities off until explicitly granted.
Also known as Security by Design, SbD, Secure-by-Design, CISA Secure by Design
Secure by design is a software engineering principle in which a product's architecture and defaults, not its users, carry the security burden. Dangerous capabilities stay off until explicitly granted, whole classes of flaws are removed by construction, and the vendor owns the outcome. The Cybersecurity and Infrastructure Security Agency (CISA) turned the principle into public commitments for software makers.
The concept is also called security by design, or SbD. It has a narrower companion, secure by default. CISA's joint guidance defines secure-by-default products as "resilient against prevalent exploitation techniques out of the box." Secure by design is how a product is built. Secure by default is the state it ships in.
The most consequential secure by design change in open source this decade shipped on July 8, 2026. npm v12 stopped running dependency install scripts unless the project approves them. Ten years earlier, npm had weighed the same risk and chosen the opposite default. Our position: in the software supply chain, secure by design means the toolchain treats every third-party package as untrusted until a person or a policy says otherwise. Frameworks that only address bugs in a vendor's own code miss the attacks that actually reach developers, because malware does not get a CVE.
What is secure by design?
#Secure by design is a property of how a system is constructed, not a checklist applied before release. The idea is older than the name. In 1975, Jerome Saltzer and Michael Schroeder published The Protection of Information in Computer Systems, which lists eight design principles that still anchor the field.
The first that matters here is fail-safe defaults: "Base access decisions on permission rather than exclusion." Saltzer and Schroeder gave the reason in one sentence, and it is the entire argument for secure defaults:
"In a large system some objects will be inadequately considered, so a default of lack of permission is safer." (Saltzer and Schroeder, 1975)
A node_modules tree with hundreds of transitive packages is exactly that large system. Nobody considers every object in it.
The second is least privilege: every program should run with "the least set of privileges necessary to complete the job." A component that holds less authority can do less damage when it fails or turns hostile.
The third comes from CISA's modern framing: eliminate classes of defects instead of chasing instances. Parameterized queries remove SQL injection as a category. Memory-safe languages remove use-after-free as a category. Patching each instance one advisory at a time is the opposite strategy, and it never finishes.
What are secure by design principles?
#Secure by design principles come in two layers: engineering principles about how systems are built, and accountability principles about who owns the result. CISA, the NSA, the FBI, and agencies from six other countries published the accountability layer on April 13, 2023 in Shifting the Balance of Cybersecurity Risk, then expanded it with more international partners in October 2023. The April 2023 version stated its first principle in one line:
"The burden of security should not fall solely on the customer." (CISA and partners, April 2023)
| Principle | Source | What it requires | Example |
|---|---|---|---|
| Fail-safe defaults | Saltzer and Schroeder, 1975 | Deny unless explicitly permitted | npm v12 blocks dependency install scripts not listed in allowScripts |
| Least privilege | Saltzer and Schroeder, 1975 | Each component gets only the authority it needs | Install-time code should not be able to read publish tokens or cloud keys |
| Eliminate vulnerability classes | CISA, 2023 | Remove categories of defects by construction | Memory-safe languages; blocking unapproved install-time execution |
| Take ownership of customer security outcomes | CISA, 2023 | The vendor ships safe defaults at no extra cost | MFA opt-out for admins, no shared default passwords, audit logs included |
| Embrace radical transparency and accountability | CISA, 2023 | Publish vulnerability data and root causes | Accurate CWE fields in CVE records; an SBOM for each release |
| Lead from the top | CISA, 2023 | Security is an executive business requirement | Leadership accepts the compatibility cost of a breaking secure default |
The guidance gets concrete. Products "should not come with default passwords that are universally shared." MFA for administrators should be opt-out rather than opt-in. Audit logs come "at no extra charge." Manufacturers should "prioritize the use of memory safe languages wherever possible." On third-party code, it asks manufacturers to acquire well-secured components and produce a software bill of materials (SBOM). Its supply chain advice is about component hygiene and visibility, not about what a build tool is allowed to execute.
The CISA Secure by Design pledge
#The CISA Secure by Design pledge launched at RSA Conference on May 8, 2024, with 68 initial signatories. Socket was one of them. By November 2024, 248 companies had signed, according to The Record. The pledge is voluntary and asks each signatory to show measurable progress on seven goals within one year of signing:
- Increase customer use of multi-factor authentication.
- Reduce default passwords.
- Significantly reduce the prevalence of one or more vulnerability classes.
- Increase customers' installation of security patches.
- Publish a vulnerability disclosure policy that authorizes public testing.
- Include accurate CWE and CPE fields in CVE records.
- Increase customers' ability to gather evidence of intrusions.
The pledge has no enforcement. CISA said it would not remove signatories that failed to report progress. Bob Lord and Lauren Zabierek, the program's architects, resigned from CISA in April 2025 amid agency cuts, and the program's future inside the agency has been unclear since.
We think the seven goals are right, and we think they are incomplete. Every goal concerns the vendor's own product: its login, its passwords, its bugs, its patches, its CVE records, its logs. None asks what the vendor's build installs, or what runs on its engineers' laptops when they install it. That gap matters because the defining open source attacks of 2025 and 2026 were not vulnerabilities. They were malware in legitimate packages, and malware does not get a CVE.
How does secure by design apply to package managers?
#Secure by design applies to package managers through their defaults, because a package manager executes third-party code on its user's behalf. To see why npm's old default existed, start with what npm install does.
npm resolves the dependency graph, downloads each tarball, extracts it into node_modules, and then runs the package's install scripts: preinstall, install, and postinstall. It also runs node-gyp rebuild implicitly for any package that ships a binding.gyp file. These hooks exist for real reasons. Native addons have to be compiled for the host, and tools such as Playwright and Cypress download binaries at install time.
The hooks run as you: same user, same environment variables, same home directory, same ~/.npmrc publish token, same cloud credentials. Here is npm 10.9.9 installing a local test package whose postinstall runs node whoami.js, a three-line script that prints its uid, user, home directory, and how many environment variables it can read:
$ npm install --foreground-scripts
> install-hook-demo@1.0.0 postinstall
> node whoami.js
postinstall: uid=0 user=root home=/root env_vars=230
added 1 package, and audited 2 packages in 312msThe script ran without a prompt, as root in our build container, with all 230 environment variables in scope. (--foreground-scripts only makes the output visible. npm 10 runs the script either way.)
On March 26, 2016, CERT/CC published VU#319816, warning that this design let a malicious author build a worm that "spreads across the majority of the npm ecosystem." CERT said it was "currently unaware of a practical solution." npm kept scripts on and explained why:
"On balance, it's npm's belief that the utility of having installation scripts is greater than the risk of worms." (npm blog, March 25, 2016)
That was a defensible trade-off in 2016. It was also a textbook violation of fail-safe defaults. Protection depended on every user knowing to opt out with --ignore-scripts, and on every package being adequately considered.
The worm CERT described arrived in September 2025. Shai-Hulud began with a malicious version of @ctrl/tinycolor, a package then downloaded more than two million times a week, and ran its payload from a postinstall hook. It harvested npm tokens and used them to republish itself into other packages the victim maintained. Shai-Hulud's second wave in November 2025 moved to preinstall and hit more than 500 packages and 700 versions.
The timeline above, as text:
| Date | Event |
|---|---|
| 1975 | Saltzer and Schroeder publish fail-safe defaults and least privilege |
| March 2016 | CERT/CC publishes VU#319816 on npm worms; npm keeps install scripts on by default |
| April 2023 | CISA, the NSA, the FBI, and partners in six other countries publish secure by design principles |
| May 2024 | CISA launches the Secure by Design pledge with 68 initial signatories |
| January 2025 | pnpm 10.0.0 stops running dependency lifecycle scripts by default |
| September 2025 | The Shai-Hulud worm spreads through a postinstall hook |
| November 2025 | Shai-Hulud's second wave moves to preinstall and hits more than 500 packages |
| December 2025 | npm permanently revokes classic tokens |
| June 2026 | Miasma waves execute through binding.gyp with no script field in package.json |
| July 2026 | npm v12 ships with dependency install scripts, git dependencies, and remote URL dependencies off by default |
npm ran dependency install scripts by default for the full ten years between the CERT warning and npm v12.
npm v12: fail-safe defaults, ten years later
#npm v12 reached general availability on July 8, 2026, and inverts the 2016 decision. Per GitHub's announcement of the v12 breaking changes, npm install "will no longer execute preinstall, install, or postinstall scripts from dependencies" unless the project allows them. Here is the same install on npm 12.2.2, the latest release as of November 2026:
$ npm install
added 1 package, and audited 2 packages in 287ms
npm warn install-scripts 1 package had install scripts blocked because they are not covered by allowScripts:
npm warn install-scripts install-hook-demo@1.0.0 (postinstall: node whoami.js)
npm warn install-scripts
npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.The figure above compares the two install paths. In npm 11 and earlier, npm resolves the graph, fetches and extracts the tarball, then runs postinstall automatically, with the installing user's uid, environment variables, ~/.npmrc token, and cloud credentials. The only way out is --ignore-scripts. In npm 12, after extraction npm checks whether that exact package version appears in allowScripts. If it does, the script runs. If it does not, npm skips the script, the install succeeds, and npm prints a warning. With --strict-allow-scripts, the install fails instead.
Approvals live in package.json, and by default they are pinned to the version you reviewed. Approving esbuild on npm 12.2.2 writes this:
{
"dependencies": { "esbuild": "^0.28.2" },
"allowScripts": { "esbuild@0.28.2": true }
}The pin is the secure by design detail. When we moved the same project to esbuild@0.28.1, npm blocked its postinstall again until we re-approved it. An approval granted for one version does not extend to a version an attacker publishes tomorrow. Bun's default allowlist works differently: its trustedDependencies field and built-in list are keyed by package name, so any version of a listed package runs its scripts.
npm v12 changed four other defaults that matter:
- The implicit
node-gyp rebuildfor packages with abinding.gypis blocked too. Attackers had already moved there. The Miasma "Phantom Gyp" waves in June 2026 ran code frombinding.gypwith no script field inpackage.json. allow-gitdefaults tonone. GitHub's announcement says this "closes a code-execution path where a Git dependency's.npmrccould override the Git executable," a path that ran code even under--ignore-scripts.allow-remotedefaults tonone, so tarball URLs on hosts other than the configured registry no longer resolve without opt-in.--strict-allow-scriptsturns unreviewed scripts into a hardESTRICTALLOWSCRIPTSerror for CI. The escape hatch is named--dangerously-allow-all-scripts, which is the right name for it.
npm was not first. Bun already ran only allowlisted dependency scripts, and pnpm 10.0.0 blocked them in January 2025 after the Rspack compromise. pnpm maintainer Zoltan Kochan said pnpm had little choice once Bun already blocked them. npm's change matters most because npm is the default client that ships with Node.js.
What do secure defaults not cover?
#Secure defaults do not cover code that runs after install, hooks outside the package manager, or attackers who publish through a legitimate account. They remove one execution path. They do not make a package trustworthy.
- Code that runs on import. A blocked
postinstallis irrelevant if the payload sits inindex.jsand runs the first time a test suite or application callsrequire(). - Developer workflow hooks. The June 2026 Miasma wave also planted VS Code folder-open tasks and AI coding assistant session hooks that fire when a developer opens a repository. No package manager setting governs those.
- Legitimate publishing pipelines. Shai-Hulud and Miasma published from compromised maintainer accounts, some with valid provenance, as we noted at the v12 launch. Provenance proves where a package was built, not that its code is benign. A maintainer account takeover can pass signature and provenance checks.
- Approval fatigue.
npm install-scripts approve --allexists. A team that runs it on every warning has rebuilt the old default with extra steps. - Contexts with no package.json. Global installs and
npxhave noallowScriptsto read. They rely on the--allow-scriptsflag or user-level config. - Release timing. npm 12 supports
min-release-agebut leaves it unset by default. pnpm 11 ships with a 24-hour minimum release age, a dependency cooldown that keeps brand-new versions out of installs.
No single control covers install-time code execution. This is how the common controls compare:
| Control | On by default | What it stops | What it misses | Cost |
|---|---|---|---|---|
| --ignore-scripts (npm 11 and earlier) | No | All lifecycle scripts, when someone remembers the flag | Pre-v12 git .npmrc path; code that runs on import | Breaks native modules with no per-package exceptions |
| Name-keyed allowlist (Bun trustedDependencies) | Yes | Scripts from packages not on the list | A malicious new version of a listed package | Low; built-in list covers popular packages |
| Version-pinned allowlist (npm v12 allowScripts) | Yes | Scripts from any unreviewed package version | Code that runs on import; IDE and agent hooks | Re-approval on every upgrade of a scripted package |
| Pre-install package analysis and blocking | No | Known malicious versions, with or without scripts | Malware not yet identified | A network dependency at install time |
| Isolated install environment without secrets | No | Credential theft during install | Code that runs later, with the application's privileges | CI and container plumbing |
Verdict: treat the version-pinned allowlist as the floor, since it is now the npm default. Run CI with --strict-allow-scripts so new scripts fail loudly. Add pre-install analysis, because recent campaigns ship code that runs whether or not an install script fires. Use isolated, secret-free install environments where the plumbing allows it.
The approval prompt also has a blind spot. It shows the command a script runs, not what the script does, a gap npm's own community raised during the v12 rollout. Socket analyzes the contents of each package version and raises alerts such as Install scripts, Network access, Environment variable access, and Shell access, so the person approving a script sees behavior instead of a command string. Socket Firewall Free (sfw npm install) blocks confirmed malware at the network layer before the tarball arrives, whether or not the package uses an install script. It does not block versions that no one has yet confirmed as malicious.
Secure by design includes code you ship but did not write
#CISA's first principle is to take ownership of customer security outcomes. Every dependency a vendor ships is code it did not write but now answers for. A vendor that owns its customers' security outcomes owns that code too, and an SBOM only records what shipped after the fact.
The registries come next. Saltzer and Schroeder's least common mechanism principle warns that every shared mechanism "represents a potential information path between users." A public package registry is the most shared mechanism in software. npm already revoked classic tokens on December 9, 2025. It plans to remove direct publishing from 2FA-bypass tokens around January 2027, according to the v12 announcement. Those tokens will be able to stage a release, but a human must approve it with 2FA before it goes public. That is the same move as v12, applied to the publish side.
The principle was never the hard part. Saltzer and Schroeder wrote down fail-safe defaults in 1975, and CERT described the npm worm in 2016. What secure by design actually demands is accepting the compatibility cost before the incident instead of after it. npm paid that cost in 2026, after the worm. The open question is which ecosystem flips its next unsafe default before its own worm arrives.
