@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 +20 -0
- package/package.json +2 -2
- package/src/inline-script-collector.ts +60 -14
- package/src/security.test.ts +30 -0
- package/src/strategy.ts +21 -1
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.
|
|
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.
|
|
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
|
-
//
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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(
|
|
109
|
-
message:
|
|
110
|
-
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: { ...
|
|
170
|
+
env: { ...safeEnv, ...config.env },
|
|
151
171
|
stdout: "pipe",
|
|
152
172
|
stderr: "pipe",
|
|
153
173
|
});
|