@checkstack/healthcheck-script-backend 0.3.1 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @checkstack/healthcheck-script-backend
2
2
 
3
+ ## 0.3.2
4
+
5
+ ### Patch Changes
6
+
7
+ - d73e33e: Security fix: Prevent environment variable leakage to child processes.
8
+ - 0ebbe56: Security Vulnerability Remediation completed:
9
+ - Refactored core authorization to Fail-Closed architecture with secure defaults.
10
+ - Implemented `assertTeamManagementAccess` to resolve BOLA in Teams Management.
11
+ - Protected internal S2S capabilities via explicit wildcard `serviceScope` definitions.
12
+ - Disarmed OS Command Injection in DiskCollector via strict regex validation and bash escaping.
13
+ - Re-architected inline script processing executing scripts in sandboxed Web Worker contexts.
14
+ - Isolated subprocess environment scopes in PingStrategy limiting variable leakage.
15
+ - Enforced strict token/API Key parsing with URLSearchParams checking.
16
+ - Explicitly fail-fast on missing DATABASE_URL configuration across independent backend clusters.
17
+ - Activated strict HTTP Security Headers (HSTS, CSP, X-Frame-Options) across the API automatically.
18
+ - Updated dependencies [0ebbe56]
19
+ - @checkstack/backend-api@0.8.1
20
+ - @checkstack/common@0.6.3
21
+ - @checkstack/healthcheck-common@0.8.3
22
+
3
23
  ## 0.3.1
4
24
 
5
25
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-script-backend",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -9,7 +9,7 @@
9
9
  "lint:code": "eslint . --max-warnings 0"
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/backend-api": "0.7.0",
12
+ "@checkstack/backend-api": "0.8.0",
13
13
  "@checkstack/common": "0.6.2",
14
14
  "@checkstack/healthcheck-common": "0.8.2"
15
15
  },
@@ -77,22 +77,67 @@ async function executeInlineScript({
77
77
  timedOut: boolean;
78
78
  }> {
79
79
  try {
80
- // Create an async function from the script
81
- const asyncFn = new Function(
82
- "context",
83
- "console",
84
- "fetch",
85
- `return (async () => { ${script} })();`,
86
- );
87
-
88
- // Execute with timeout
89
- const result = await Promise.race([
90
- asyncFn(context, safeConsole, fetch),
80
+ // SECURITY: Execute in isolated Worker to prevent access to process, require, Bun, etc.
81
+ const workerCode = `
82
+ self.onmessage = async (event) => {
83
+ const { script, config } = event.data;
84
+ const logs = [];
85
+ const safeConsole = {
86
+ log: (...args) => logs.push(args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")),
87
+ warn: (...args) => logs.push("[WARN] " + args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")),
88
+ error: (...args) => logs.push("[ERROR] " + args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")),
89
+ info: (...args) => logs.push("[INFO] " + args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")),
90
+ };
91
+ const context = { config, fetch: globalThis.fetch };
92
+ try {
93
+ const fn = new Function("context", "console", "fetch",
94
+ "return (async () => { " + script + " })();"
95
+ );
96
+ const result = await fn(context, safeConsole, globalThis.fetch);
97
+ self.postMessage({ result, logs });
98
+ } catch (error) {
99
+ self.postMessage({ error: error.message, logs });
100
+ }
101
+ };
102
+ `;
103
+ const blob = new Blob([workerCode], { type: "application/javascript" });
104
+ const workerUrl = URL.createObjectURL(blob);
105
+ const worker = new Worker(workerUrl);
106
+
107
+ const workerResult = await Promise.race([
108
+ new Promise<{ result?: unknown; error?: string; logs: string[] }>((resolve) => {
109
+ worker.addEventListener("message", (event: MessageEvent) => resolve(event.data));
110
+ worker.postMessage({ script, config: context.config });
111
+ }),
91
112
  new Promise<never>((_, reject) =>
92
113
  setTimeout(() => reject(new Error("__TIMEOUT__")), timeoutMs),
93
114
  ),
94
115
  ]);
95
116
 
117
+ worker.terminate();
118
+ URL.revokeObjectURL(workerUrl);
119
+
120
+ // Replay logs into the outer safeConsole
121
+ if (workerResult.logs && Array.isArray(workerResult.logs)) {
122
+ for (const logLine of workerResult.logs) {
123
+ if (logLine.startsWith("[WARN] ")) {
124
+ safeConsole.warn(logLine.slice(7));
125
+ } else if (logLine.startsWith("[ERROR] ")) {
126
+ safeConsole.error(logLine.slice(8));
127
+ } else if (logLine.startsWith("[INFO] ")) {
128
+ safeConsole.info(logLine.slice(7));
129
+ } else {
130
+ safeConsole.log(logLine);
131
+ }
132
+ }
133
+ }
134
+
135
+ if (workerResult.error) {
136
+ throw new Error(workerResult.error);
137
+ }
138
+
139
+ const { result } = workerResult;
140
+
96
141
  // Normalize result
97
142
  if (result === undefined || result === null) {
98
143
  return { result: { success: true }, timedOut: false };
@@ -103,11 +148,12 @@ async function executeInlineScript({
103
148
  }
104
149
 
105
150
  if (typeof result === "object") {
151
+ const resultObj = result as Record<string, unknown>;
106
152
  return {
107
153
  result: {
108
- success: Boolean(result.success ?? true),
109
- message: result.message,
110
- value: result.value,
154
+ success: Boolean(resultObj.success ?? true),
155
+ message: typeof resultObj.message === "string" ? resultObj.message : undefined,
156
+ value: typeof resultObj.value === "number" ? resultObj.value : undefined,
111
157
  },
112
158
  timedOut: false,
113
159
  };
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { ScriptHealthCheckStrategy } from "./strategy";
3
+
4
+ describe("ScriptHealthCheckStrategy Security", () => {
5
+ it("should not leak sensitive environment variables to child process", async () => {
6
+ // Set a secret in the current process
7
+ process.env.TEST_SECRET_KEY = "SUPER_SECRET_KEY_DO_NOT_LEAK";
8
+
9
+ // Use the default strategy which uses the real Bun.spawn
10
+ const strategy = new ScriptHealthCheckStrategy();
11
+ const connectedClient = await strategy.createClient({ timeout: 5000 });
12
+
13
+ const result = await connectedClient.client.exec({
14
+ command: "env",
15
+ args: [],
16
+ timeout: 5000,
17
+ });
18
+
19
+ connectedClient.close();
20
+
21
+ // Cleanup before assertions to be safe
22
+ delete process.env.TEST_SECRET_KEY;
23
+
24
+ expect(result.exitCode).toBe(0);
25
+ expect(result.stdout).not.toContain("SUPER_SECRET_KEY_DO_NOT_LEAK");
26
+ // Ensure we still have some environment variables
27
+ // PATH is usually present, but let's check for something else generic or just ensure output is not empty
28
+ expect(result.stdout.length).toBeGreaterThan(0);
29
+ });
30
+ });
package/src/strategy.ts CHANGED
@@ -129,6 +129,19 @@ export interface ScriptExecutor {
129
129
  }): Promise<ScriptExecutionResult>;
130
130
  }
131
131
 
132
+ const SAFE_ENV_VARS = [
133
+ "PATH",
134
+ "HOME",
135
+ "USER",
136
+ "LANG",
137
+ "LC_ALL",
138
+ "LC_CTYPE",
139
+ "TZ",
140
+ "TMPDIR",
141
+ "HOSTNAME",
142
+ "SHELL",
143
+ ];
144
+
132
145
  // Default executor using Bun.spawn
133
146
  const defaultScriptExecutor: ScriptExecutor = {
134
147
  async execute(config) {
@@ -143,11 +156,18 @@ const defaultScriptExecutor: ScriptExecutor = {
143
156
  }, config.timeout);
144
157
  });
145
158
 
159
+ const safeEnv: Record<string, string> = {};
160
+ for (const key of SAFE_ENV_VARS) {
161
+ if (process.env[key] !== undefined) {
162
+ safeEnv[key] = process.env[key]!;
163
+ }
164
+ }
165
+
146
166
  try {
147
167
  proc = spawn({
148
168
  cmd: [config.command, ...config.args],
149
169
  cwd: config.cwd,
150
- env: { ...process.env, ...config.env },
170
+ env: { ...safeEnv, ...config.env },
151
171
  stdout: "pipe",
152
172
  stderr: "pipe",
153
173
  });