Skip to content

Security model

The kit follows untrusted input through three boundaries: HTTP (manifests + downloads), filesystem (archive extraction + atomic writes), and child processes (Forge processors + launch). This page documents every defence in place — useful when pen-testing your launcher or threat-modelling a hostile network.

Downloads

URL scheme allow-list

downloadFile rejects any URL that isn't parseable or whose scheme is not https: or http:. Closes the manifest-injection class — a malicious Mojang/Forge manifest cannot coax fetch into following file://, data:, or javascript: URLs.

ts
// throws MinecraftKitError("INVALID_INPUT") before any network call
await downloadFile(http, { url: "file:///etc/passwd", target });

Host allow-list (optional)

For launchers shipping in a hostile environment (corporate proxies, captured DNS, etc.), pass hostAllowList to pin downloads to a known set of hosts. Entries support exact host or a leading wildcard label:

ts
await kit.install.run(plan, {
  // ...passed through to downloadFile internally on a future API; for now:
});

// At the lower-level API:
await downloadFile(http, {
  url: action.url,
  target: action.target,
  hostAllowList: [
    "*.minecraft.net",
    "*.minecraftservices.com",
    "*.mojang.com",
    "maven.minecraftforge.net",
    "libraries.minecraft.net",
  ],
});

When set, anything outside the list throws INVALID_INPUT before fetch, with error.context.host carrying the rejected hostname.

Manifests

Network JSON passes through lightweight runtime predicates in src/core/guards.ts before the code trusts it. Currently enforced:

  • MinecraftVersionManifest shape on kit.versions.minecraft.resolve() — id, mainClass, assetIndex (id + sha1 + size + url), and downloads.client (sha1 + size + url) are all required and type-checked.
  • INTEGRITY_HASH_MISMATCH / INTEGRITY_SIZE_MISMATCH at the download boundary — the downloader computes sha1 on the fly and rejects bytes that don't match the manifest.

Predicates are deliberately permissive on field values (e.g. sha1 is checked as a string, not strict 40-char hex) because legacy Mojang manifests sometimes ship placeholder hashes. Integrity is enforced at download time, not parse time.

Add new guards to src/core/guards.ts and call parseJsonAs(text, guard, { code, message }) at the boundary.

Archives

Zip/jar handling in src/core/archive.ts defends against:

AttackDefence
Zip slip (../etc/passwd)assertWithinRoot rejects extracted paths that resolve outside the target directory.
Absolute paths inside the zipassertSafeEntryName rejects /etc/passwd, C:\..., and Windows drive letters.
Null-byte injectionEntry names containing \0 are rejected.
Reserved Windows names (CON, NUL, …)Rejected.
Trailing dot / whitespaceRejected (Windows would silently strip and re-target).
Zip bomb (entries)EXTRACTION_MAX_ENTRIES cap.
Zip bomb (per-entry size)EXTRACTION_MAX_ENTRY_SIZE cap.
Zip bomb (total uncompressed size)EXTRACTION_MAX_TOTAL_SIZE cap.
Zip bomb (compression ratio)EXTRACTION_MAX_COMPRESSION_RATIO cap.

All four caps live in src/constants/limits.ts.

Filesystem writes

atomicWrite(path, content) writes to a sibling temp file then renames over the destination. A crash mid-write leaves either the old file or the new one, never a partial write. The same atomic pattern is used by downloadFile's temp <target>.<random>.download that gets fs.renamed only after hash + size checks pass.

Child processes

runProcessor (Forge installer steps) and runLaunch (Minecraft itself) both go through the injected Spawner. The default ChildProcessSpawner:

  • Never sets shell: true. Arguments are passed as an array so the OS shell never expands them.
  • Passes the resolved Java path absolute (computed via targetPaths.runtimeJavaExecutable) — the user's PATH cannot redirect the launch.
  • Caps line buffers at SPAWNER_MAX_LINE_BYTES so a malicious processor cannot OOM the launcher with one giant line.
  • The Forge processor lifecycle (runProcessor) verifies every declared output sha1 before continuing — a processor cannot smuggle replacement artifacts into the install.

Authentication

Tokens never touch disk. kit.auth.login() returns a session; storing the refresh token is the caller's job. The kit ships zero default credentials — MINECRAFT_KIT_MSA_CLIENT_ID must be set or the caller passes clientId explicitly. Auth trace can leak token lengths (not contents); it stays silent unless a Logger is wired or MINECRAFT_KIT_AUTH_DEBUG=1 is set.

What the kit does NOT defend against

  • Compromised upstream manifests with valid signatures. If Mojang signs a manifest that points at a sha1 the attacker controls, the integrity check passes. The kit cannot do anything about an upstream supply-chain compromise — your only defence is the host allow-list to keep an attacker from re-pointing downloads at an off-prem host.
  • Malicious mod jars run by the Forge processors. Forge processors are arbitrary Java code that Mojang/Forge tell us to run. We sandbox the output (sha1 checks the produced files), but the processor itself runs with whatever permissions the launcher has.
  • The Minecraft process itself. Once runLaunch spawns the child, it is in user-space alongside the launcher. Sandbox the child via OS facilities if you need stronger isolation.

MIT License