sync: update from private repo (e62f5c7)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-11 01:52:48 +00:00
parent 000a2474aa
commit d061ad08d8
237 changed files with 8441 additions and 5549 deletions
+48 -3
View File
@@ -168,10 +168,25 @@ export function createCoreServer(opts: CoreServerOptions): {
* read current state for the admin status endpoint.
*/
gatewayMount: GatewayMountHandle | null;
/** True when an OAuth provider or local auth is active. False = no-auth mode. */
authActive: boolean;
} {
const { repo, worktreeDir } = opts;
const app = express();
// Don't advertise the framework/version (minor info-leak, free to drop).
app.disable('x-powered-by');
// Baseline hardening header on every response. `nosniff` stops the browser
// from MIME-sniffing a response into something executable. File-serving
// endpoints add `Content-Security-Policy: sandbox` on top
// (see setUntrustedFileResponseHeaders) to neutralize stored XSS from
// agent/user-authored workspace files.
app.use((_req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
});
// リバースプロキシ背後で secure cookie / X-Forwarded-Proto を正しく処理
if (opts.authConfig?.secureCookie) {
app.set('trust proxy', 1);
@@ -816,9 +831,13 @@ export function createCoreServer(opts: CoreServerOptions): {
// POST /api/local/tasks/:taskId/console/session. Reuses the same
// SshSubsystem + preflight the console tools use; the access gate
// runs inside openConsoleSession against task.pieceName.
// NOTE: no express.json() here — the router is mounted on the whole
// /api prefix, so a mount-level parser (default limit 100kb) would
// run for EVERY /api request and 413 large bodies before the
// route-specific parsers (e.g. the task-attachment limit) ever ran.
// The session route carries its own scoped json() parser.
app.use(
'/api',
express.json(),
createConsoleSessionRouter({
sub: sshSubsystem,
preflight: sshPreflight,
@@ -1157,7 +1176,7 @@ export function createCoreServer(opts: CoreServerOptions): {
return isOwner || user.role === 'admin';
};
return { app, browserSessionManager, authenticateUpgrade, authorizeNovncSession, sshConsole, backendStatusRegistry, workerMetrics, gatewayMount };
return { app, browserSessionManager, authenticateUpgrade, authorizeNovncSession, sshConsole, backendStatusRegistry, workerMetrics, gatewayMount, authActive };
}
export function finalizeServer(app: express.Application): express.Application {
@@ -1166,6 +1185,15 @@ export function finalizeServer(app: express.Application): express.Application {
});
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
// body-parser signals an exceeded json({ limit }) with type
// 'entity.too.large'. Surface it as the 413 it is — collapsing it into a
// generic 500 hides the actual problem ("attachment over the configured
// limit") from API clients and the UI.
if ((err as { type?: string }).type === 'entity.too.large') {
logger.warn(`Request body too large: ${err.message}`);
res.status(413).json({ error: 'Request body too large' });
return;
}
logger.error(`Unhandled error: ${err.message}`);
res.status(500).json({ error: 'Internal server error' });
});
@@ -1202,6 +1230,7 @@ export function startCoreServer(opts: CoreServerOptions, port: number = 9876): v
backendStatusRegistry,
workerMetrics,
gatewayMount,
authActive,
// Forward the actual port to createCoreServer so the admin gateway
// status endpoint reports the real bind port (not the PORT env
// guess). See `listenPort` doc on CoreServerOptions.
@@ -1218,9 +1247,25 @@ export function startCoreServer(opts: CoreServerOptions, port: number = 9876): v
opts.workerManager.setWorkerMetrics(workerMetrics);
}
const finalApp = finalizeServer(app);
const host = process.env['HOST'] ?? '0.0.0.0';
// Safe-by-default bind: loopback unless HOST is set explicitly. The agent API
// includes the Bash tool, so an unauthenticated instance reachable on the LAN
// is effectively unauthenticated RCE. Bare-metal / systemd installs inherit
// this loopback default; the Docker image sets HOST=0.0.0.0 (it must bind all
// interfaces inside the container) and is protected by the host-side
// 127.0.0.1:9876 port mapping instead.
const host = process.env['HOST'] ?? '127.0.0.1';
const isLoopbackBind = host === '127.0.0.1' || host === '::1' || host === 'localhost';
const server = finalApp.listen(port, host, () => {
logger.info(`Core server listening on ${host}:${port}`);
if (!isLoopbackBind && !authActive) {
logger.warn(
`[security] Listening on ${host} with authentication DISABLED. The agent API ` +
`(including the Bash tool) is reachable by anyone who can reach this host — ` +
`this is effectively unauthenticated remote code execution. Enable auth in ` +
`config.yaml (auth.local or an OAuth provider) before exposing a non-loopback ` +
`interface, or unset HOST to bind 127.0.0.1.`,
);
}
});
// 起動と同時に CAPTCHA Pool の idle GC を回す (task session を 5 分アイドルで GC)