kern-sandbox 0.1.13 → 0.1.14

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 (3) hide show
  1. package/README.md +4 -0
  2. package/index.js +36 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -134,6 +134,10 @@ new Sandbox({
134
134
  pids, // default 256
135
135
  timeoutS, // default 30, MANDATORY per-call deadline
136
136
  network, // default false (RELAXES ISOLATION)
137
+ capDrop, // default ["ALL"]: capabilities dropped from every box. kern always drops
138
+ // 13 dangerous ones; this drops the rest, which were held over the box's own
139
+ // user namespace. Pass [] to keep them (needed only if the workload binds a
140
+ // port below 1024 INSIDE the box).
137
141
  mounts, // { hostSrc: boxTarget } or { src: [target, "ro"] }
138
142
  profiles, // reusable kern.toml profiles: ["vcpu:heavy", "vgpio:leds", "vdisk:scratch"]
139
143
  env, // { KEY: "value" }
package/index.js CHANGED
@@ -36,7 +36,7 @@ const crypto = require("crypto");
36
36
  const zlib = require("zlib");
37
37
  const { spawn, spawnSync } = require("child_process");
38
38
 
39
- const VERSION = "0.1.13";
39
+ const VERSION = "0.1.14";
40
40
 
41
41
  const DEFAULT_IMAGE = "python:3.12-slim";
42
42
  const WORKSPACE = "/workspace"; // where the persistent workspace is mounted inside every box
@@ -552,6 +552,12 @@ function validateProfile(token) {
552
552
  // re-validates and SSRF-checks the resolved IPs; this is the binding's first gate.
553
553
  const DOMAIN_RE = /^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$/;
554
554
 
555
+ // A Linux capability name for `kern box --cap-drop`, with or without the CAP_ prefix, or the literal
556
+ // ALL. Underscore-JOINED uppercase segments rather than "any of [A-Z0-9_]": the looser form accepts
557
+ // "CAP_", because the optional prefix does not have to consume it. Not a way to smuggle a flag, but
558
+ // a name kern rejects at box start, and validating here exists to fail at construction instead.
559
+ const CAP_RE = /^(?=.{1,32}$)(?:CAP_)?[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/;
560
+
555
561
  /** Validate one egress-allowlist domain (an FQDN like "pypi.org") before it reaches the argv. */
556
562
  function validateDomain(domain) {
557
563
  if (typeof domain !== "string" || !DOMAIN_RE.test(domain))
@@ -562,6 +568,16 @@ function validateDomain(domain) {
562
568
  return domain;
563
569
  }
564
570
 
571
+ /** Validate one capability name for `--cap-drop` before it reaches the argv. */
572
+ function validateCap(name) {
573
+ if (typeof name !== "string" || !CAP_RE.test(name))
574
+ throw new SandboxError(
575
+ `invalid capability ${JSON.stringify(name)}: expected 'ALL' or an uppercase capability name ` +
576
+ "such as 'NET_BIND_SERVICE' or 'CAP_NET_BIND_SERVICE'",
577
+ );
578
+ return name;
579
+ }
580
+
565
581
  /** Map a Node close event {code, signal} to a unix-style rc (128 + signum for a signal). */
566
582
  function toRc(code, signal) {
567
583
  if (typeof code === "number") return code;
@@ -765,6 +781,13 @@ class Sandbox {
765
781
  this.onStderr = opts.onStderr ?? null;
766
782
  this.enforceLimits = opts.enforceLimits ?? true;
767
783
  this.depsReadonly = opts.depsReadonly ?? false;
784
+ // Capabilities dropped from every box this sandbox starts, as kern's own `--cap-drop` takes them.
785
+ // The default drops the lot: kern already drops 13 dangerous capabilities unconditionally, but the
786
+ // rest were still held over the box's own user namespace, on the one code path whose purpose is
787
+ // running code nobody has read. Defence in depth rather than the boundary itself, and measured to
788
+ // cost nothing. It is NOT behaviour-free: a workload binding a port below 1024 INSIDE the box
789
+ // needs CAP_NET_BIND_SERVICE. Pass `capDrop: []` for the previous behaviour.
790
+ this.capDrop = opts.capDrop ?? ["ALL"];
768
791
  // trackFiles=true populates result.files by walking the workspace before AND after each call (O(N)
769
792
  // in file count); a long session that accretes files slows every runCode. false = result.files [], O(1).
770
793
  this.trackFiles = opts.trackFiles ?? true;
@@ -790,6 +813,17 @@ class Sandbox {
790
813
  this._mountArgs.push("-v", ro ? `${real}:${tgt}:ro` : `${real}:${tgt}`);
791
814
  }
792
815
  }
816
+ // A bare string has a .map-less shape here, but Array.from("ALL") would yield ["A","L","L"] and
817
+ // three bogus flags, so refuse the string by name and say what to write instead.
818
+ if (typeof this.capDrop === "string")
819
+ throw new SandboxError(
820
+ `capDrop must be an array of names, not a bare string: write capDrop: [${JSON.stringify(
821
+ this.capDrop,
822
+ )}] for one, or capDrop: [] to drop none`,
823
+ );
824
+ if (!Array.isArray(this.capDrop))
825
+ throw new SandboxError("capDrop must be an array of capability names");
826
+ this._capDropArgs = this.capDrop.flatMap((c) => ["--cap-drop", validateCap(c)]);
793
827
  this._profileArgs = (this.profiles || []).map(validateProfile);
794
828
  this._egressAllow = (this.egressAllow || []).map(validateDomain);
795
829
  if (this._egressAllow.length && this.network)
@@ -881,6 +915,7 @@ class Sandbox {
881
915
  }
882
916
  }
883
917
  // kern's own --timeout is a tight BACKSTOP just beyond our deadline; OUR wait is the authority.
918
+ argv.push(...this._capDropArgs);
884
919
  argv.push("--timeout", String(Math.floor(timeoutS) + 5));
885
920
  if (this.memoryMb !== null) argv.push("--memory", `${this.memoryMb}m`);
886
921
  if (this.cpus !== null) argv.push("--cpus", String(this.cpus));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kern-sandbox",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "kern is a fast, rootless sandbox and virtual resource runtime for any workload, including untrusted and AI-generated code; kern-sandbox is its Node/TypeScript binding. Run untrusted or agent-generated code (Python/JS/Bash) in a real, kernel-enforced box in single-digit milliseconds, with no cloud, no account and no VM.",
5
5
  "keywords": [
6
6
  "sandbox",