Repository navigation
security: host-controlled per-package capability enforcement (compile-time) #501
Description
Activity
The headline supply-chain feature. Most npm packages will never declare capabilities themselves — control sits entirely in the host app's
package.json. The compiler walks each imported package's HIR, cross-references stdlib call sites against the host's policy, and refuses to compile on violation. Package authors do nothing.Example host config
{
"perry": {
"permissions": {
"lodash": [],
"axios": ["net:https://api.example.com"],
"left-pad": [],
"@scope/utils": ["crypto"],
"*": ["crypto"]
}
}
}
Iflodash's compiled HIR contains a call tofs.readFile, the build fails at the offending line:
lodash calls fs.readFile at node_modules/lodash/template.js:42 but is not granted fs:read in your perry.permissions; either add the capability or replace the dep.Most real-world npm supply-chain incidents (event-stream, ua-parser-js, several 2023/2024 incidents) would be caught by a default-deny policy on
*.Mechanism
HIR pass per imported source module; cross-reference each stdlib call site against the host's policy; fail compilation on violation. Zero runtime cost — pure compile-time refusal, no FFI-boundary checks emitted.
Once again, analyzing code is liable to allow vulnerabilities or severely restrict code. My take is that the safest option is to emit runtime checks that get optimized out in reasonable cases.
The user's own root code defaults to
*(full capability); per-dep entries override the*default.Capability tokens (initial set)
fs:read,fs:write,fs:execnet:fetch,net:listen,net:<host>(literal or glob)proc:env,proc:exec,proc:argvcryptotime(Date.now / performance.now — optional, side-channel paranoia)*for unrestrictedToken taxonomy is extensible as Perry's stdlib grows.
Acceptance
- Host
package.jsonperry.permissions: { "<pkg>": ["cap1", "cap2"], "*": ["default"] }[ ] HIR pass walks each dep's source modules and cross-references stdlib calls[ ] Violation fails build at the offending source span (file + line + capability + missing token)[ ] User's own root code defaults to*, configurable[ ]perry audit(separate issue) shows which capabilities each dep would need to compile cleanly[ ] Wildcard host patterns:"net:*.example.com","net:https://api.acme.com/v1/*"[ ] Nested deps inherit policy: a transitive call fromlodashthat lands infs.readFileis attributed to whichever package's source module contains the call site[ ] Initial capability tokens documented + extension path defined[ ] Composes with the URL allowlist issue (capabilitynet:<host>and the URL allowlist enforce the same constraint at different granularities)Part of the supply-chain hardening series. The big lever. Host-app-controlled. Zero runtime cost (compile-time analysis only).
For context on my similar project, dreamcomp, the approach is to have a rich API for intercepting the APIs used by dependencies; we support plugins, are planning to have an effect system, and plan to have direct "override" APIs. This limits the blast radius of any module--it can only do the intersection of all certifications from all involved modules and all APIs given (ESM imports, parameters). jsaw will be the open-source, more app-oriented, implementation (with dreamcomp compatibility), using jsaw-core as a shared library
I will note that both projects are much more focused on safety (#855 cough cough) and purity than Perry; code is more reliable, but needs more customization and independent implementation (instead of inbuilt app APIs, for instance, everything happens either in pure JS or FFI)
The headline supply-chain feature. Most npm packages will never declare capabilities themselves — control sits entirely in the host app's
package.json. The compiler walks each imported package's HIR, cross-references stdlib call sites against the host's policy, and refuses to compile on violation. Package authors do nothing.Example host config
{ "perry": { "permissions": { "lodash": [], "axios": ["net:https://api.example.com"], "left-pad": [], "@scope/utils": ["crypto"], "*": ["crypto"] } } }If
lodash's compiled HIR contains a call tofs.readFile, the build fails at the offending line:Most real-world npm supply-chain incidents (event-stream, ua-parser-js, several 2023/2024 incidents) would be caught by a default-deny policy on
*.Mechanism
HIR pass per imported source module; cross-reference each stdlib call site against the host's policy; fail compilation on violation. Zero runtime cost — pure compile-time refusal, no FFI-boundary checks emitted.
The user's own root code defaults to
*(full capability); per-dep entries override the*default.Capability tokens (initial set)
fs:read,fs:write,fs:execnet:fetch,net:listen,net:<host>(literal or glob)proc:env,proc:exec,proc:argvcryptotime(Date.now / performance.now — optional, side-channel paranoia)*for unrestrictedToken taxonomy is extensible as Perry's stdlib grows.
Acceptance
package.jsonperry.permissions: { "<pkg>": ["cap1", "cap2"], "*": ["default"] }*, configurableperry audit(separate issue) shows which capabilities each dep would need to compile cleanly"net:*.example.com","net:https://api.acme.com/v1/*"lodashthat lands infs.readFileis attributed to whichever package's source module contains the call sitenet:<host>and the URL allowlist enforce the same constraint at different granularities)Part of the supply-chain hardening series. The big lever. Host-app-controlled. Zero runtime cost (compile-time analysis only).