mirror of
https://github.com/whyour/qinglong.git
synced 2026-06-28 02:45:08 +08:00
Audit of the backend attack surface and fixes for the web-reachable CRITICAL/HIGH issues. Adds back/shared/security.ts with centralized hardening helpers (shellEscape, assertSafeDependenceName, SUBSCRIPTION_PATTERNS, safeCompare, isSafeSshConfigValue). - Subscription fields (url/branch/whitelist/blacklist/extensions/proxy) are now shell-escaped before reaching spawn() and validated with strict Joi patterns at the API, closing OS command injection and the downstream shell eval/git-arg-injection paths. - Dependency names are validated before interpolation into pnpm/pip/apk/apt commands (incl. the embedded Python source). - SSH config generation rejects newline/metachar injection in host/proxy (prevents injected ProxyCommand execution). - ConfigService.getFile resolves the real path before containment check, fixing data/scripts/../db traversal that leaked the SQLite DB. - /configs/save containment check fixed (sibling-dir write bypass). - Script/env uploads use path.basename, preventing arbitrary file write (crontab.list/env.sh overwrite -> RCE) via multer originalname. - JWT secret is generated and persisted per-install instead of the public default 'whyour-secret'; production refuses to boot without one. - Token comparison is now constant-time (safeCompare). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
73 lines
3.0 KiB
TypeScript
73 lines
3.0 KiB
TypeScript
/**
|
|
* Centralized input-hardening helpers used to neutralize command injection,
|
|
* argument injection and config-file injection across the backend.
|
|
*
|
|
* The task runner legitimately executes user scripts, but data fields such as
|
|
* git URLs, branches, dependency names and proxies are NOT meant to be code.
|
|
* These helpers keep such values from breaking out of the shell commands /
|
|
* config files they are interpolated into.
|
|
*/
|
|
import crypto from 'crypto';
|
|
|
|
/**
|
|
* POSIX single-quote escaping. Wraps a value so it is passed to the shell as a
|
|
* single literal argument, neutralizing $(), backticks, ;, |, &, redirects,
|
|
* whitespace and newlines.
|
|
*/
|
|
export function shellEscape(value: unknown): string {
|
|
const str = value === undefined || value === null ? '' : String(value);
|
|
return `'${str.replace(/'/g, `'\\''`)}'`;
|
|
}
|
|
|
|
/** Characters that enable shell command injection / argument chaining. */
|
|
const SHELL_METACHAR = /[;&|`$<>(){}\[\]'"\\!\n\r\t ]/;
|
|
|
|
/**
|
|
* Validate a package/dependence name before it is interpolated into an
|
|
* install/uninstall/version command (pnpm/pip/apk/apt). Throws on anything that
|
|
* could break out of the command or inject an extra argument.
|
|
*/
|
|
export function assertSafeDependenceName(name: string): string {
|
|
const n = String(name ?? '').trim();
|
|
if (!n || n.length > 214 || SHELL_METACHAR.test(n) || n.startsWith('-')) {
|
|
throw new Error('Invalid dependence name');
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/**
|
|
* Joi-compatible patterns for subscription fields that flow into shell commands
|
|
* and ssh config files. They block shell metacharacters / newlines while still
|
|
* permitting legitimate values.
|
|
*/
|
|
export const SUBSCRIPTION_PATTERNS = {
|
|
// git/http(s)/ssh url: no whitespace or shell metacharacters, must not start with '-'
|
|
url: /^(?!-)[^\s;&|`$<>(){}'"\\]+$/,
|
|
// git ref: word chars, dots, slashes, dashes
|
|
branch: /^[\w.\/-]*$/,
|
|
// space/comma separated bare file extensions
|
|
extensions: /^[A-Za-z0-9 ,]*$/,
|
|
// host:port (consumed by the nc ProxyCommand) or empty
|
|
proxy: /^([\w.\-]+:\d+)?$/,
|
|
// regex filters: forbid line breaks and command-substitution chars, keep regex metachars
|
|
filter: /^[^\r\n`$\\]*$/,
|
|
};
|
|
|
|
/**
|
|
* Constant-time string comparison for tokens / secrets / passwords. Both inputs
|
|
* are hashed to a fixed length first so timingSafeEqual never throws on length
|
|
* mismatch and the comparison does not leak length via timing.
|
|
*/
|
|
export function safeCompare(a: string | undefined | null, b: string | undefined | null): boolean {
|
|
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
|
const ha = crypto.createHash('sha256').update(a).digest();
|
|
const hb = crypto.createHash('sha256').update(b).digest();
|
|
return crypto.timingSafeEqual(ha, hb) && a.length === b.length;
|
|
}
|
|
|
|
/** Reject values that are dangerous when written verbatim into an ssh config file. */
|
|
export function isSafeSshConfigValue(value: string | undefined | null): boolean {
|
|
if (value === undefined || value === null) return true;
|
|
return !/[\r\n;&|`$<>()'"\\]/.test(String(value));
|
|
}
|