mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 11:47:11 +00:00
security: pre-release hardening, automated testing, and production readiness
SECURITY (critical fixes):
- Add authMiddleware to /api/system/console-token (was publicly accessible)
- Validate api_url on node create/update to prevent SSRF (rejects localhost/loopback)
- Add rate limiting (5 req/15 min/IP) to /api/auth/login and /api/auth/setup
- Fix path traversal in env_file resolution — absolute/escaping paths rejected
- Add stack name validation to GET routes (was only on PUT/POST)
- Add helmet security headers middleware
- Restrict CORS to FRONTEND_URL in production
PRODUCTION READINESS:
- Add GET /api/health public endpoint + HEALTHCHECK in Dockerfile
- Add SIGTERM/SIGINT graceful shutdown handler (drains connections, closes DB)
- Run container as non-root sencho user in Dockerfile
QUALITY:
- Fix 4 silent empty catch{} blocks in EditorLayout (now show toast.error)
- Connect ErrorBoundary to root App in main.tsx
- Replace WebSocket.Server with named WebSocketServer import (ESM compat)
TESTING (new automated test suite):
- Install Vitest; 38 backend tests across 4 suites covering validation utilities,
health endpoint, auth middleware, login flows, SSRF protection, and path traversal
- Extract isValidStackName/isValidRemoteUrl/isPathWithinBase to utils/validation.ts
- Playwright E2E scaffolding: auth, stacks, nodes specs + shared login helper
- CI: run Vitest + ESLint on every PR
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Stack name must only contain URL-safe characters with no path separators.
|
||||
* Prevents path-traversal attacks when the name is used to build filesystem paths.
|
||||
*/
|
||||
export const isValidStackName = (name: string): boolean =>
|
||||
/^[a-zA-Z0-9_-]+$/.test(name);
|
||||
|
||||
/**
|
||||
* Validates that a remote node API URL is a safe, well-formed HTTP/HTTPS URL.
|
||||
* Rejects loopback addresses to prevent SSRF against local services.
|
||||
* Private/LAN IPs are allowed — users legitimately point Sencho at nodes on their LAN.
|
||||
*/
|
||||
export function isValidRemoteUrl(
|
||||
raw: string,
|
||||
): { valid: true; url: URL } | { valid: false; reason: string } {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(raw);
|
||||
} catch {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'API URL must be a valid URL (e.g. https://my-server.example.com:3000)',
|
||||
};
|
||||
}
|
||||
if (!['http:', 'https:'].includes(url.protocol)) {
|
||||
return { valid: false, reason: 'API URL must use http:// or https://' };
|
||||
}
|
||||
// Node.js URL API preserves brackets for IPv6: new URL('http://[::1]').hostname === '[::1]'
|
||||
const loopback = /^(localhost|127(\.\d+){3}|\[::1\]|0\.0\.0\.0)$/i;
|
||||
if (loopback.test(url.hostname)) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'API URL cannot point to localhost or loopback — use the actual host address',
|
||||
};
|
||||
}
|
||||
return { valid: true, url };
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a resolved file path stays within a given base directory.
|
||||
* Returns true if the path is safe, false if it escapes the base.
|
||||
*/
|
||||
export function isPathWithinBase(resolvedPath: string, baseDir: string): boolean {
|
||||
const normalizedBase = path.resolve(baseDir);
|
||||
const normalizedPath = path.resolve(resolvedPath);
|
||||
return (
|
||||
normalizedPath === normalizedBase ||
|
||||
normalizedPath.startsWith(normalizedBase + path.sep)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user