Files
floci-ui/packages/api/src/index.ts
T
Zakaria c5a2fc9105 fix(api): block open redirects, add health check and body limits, run as non-root
- safeReturnTo resolves the target against a placeholder origin and rejects
  backslashes and control characters, so `/\evil.example` and similar
  payloads can no longer redirect off-site after sign-in.
- Add a real unauthenticated /api/health and answer unknown /api/* paths
  with a JSON 404 instead of the SPA shell.
- Limit request bodies: 16 KB for /api/auth/* (read before a session
  exists), 128 MB for resource create/update and uploads, 10 MB elsewhere.
- Run the production image as an unprivileged user and add a Docker
  HEALTHCHECK against /api/health.
2026-10-23 10:43:41 -04:00

94 lines
3.3 KiB
TypeScript

import "dotenv/config";
import { Hono } from "hono";
import { serveStatic } from "hono/bun";
import { bodyLimit } from "hono/body-limit";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import {authConfig, authMiddleware, authRoutes} from './auth';
import eks from "./routes/eks";
import ec2 from "./routes/ec2";
import secretsmanager from "./routes/secretsmanager";
import clouds from "./routes/clouds";
import msk from "./routes/msk";
import mskConnect from "./routes/mskConnect";
import ses from "./routes/ses";
import elasticache from "./routes/elasticache";
const app = new Hono();
const authentication = authConfig();
// Secrets Manager and KMS crypto routes handle sensitive values with server-side
// AWS credentials, so an unrestricted `cors()` would let any web page in the
// browser drive them cross-origin. Restrict CORS to trusted origins for those
// routes and keep the permissive default elsewhere. In production the frontend
// is served from the same origin (see serveStatic below), so same-origin requests
// are unaffected; cross-origin callers must be explicitly allow-listed.
//
// A single CORS middleware handles both cases: stacking two `cors()` calls
// would either short-circuit the OPTIONS preflight in the wrong handler or let
// the later one overwrite `Access-Control-Allow-Origin` on real requests.
const trustedOrigins = (
process.env.CORS_ALLOWED_ORIGINS ?? "http://localhost:3000"
)
.split(",")
.map((origin) => origin.trim())
.filter(Boolean);
app.use(
"*",
cors({
origin: (origin, c) =>
isSensitiveCredentialRoute(c.req.path)
? trustedOrigins.includes(origin)
? origin
: null
: "*",
}),
);
app.use("*", logger());
// Sign-in endpoints read the body before a session exists, so they get a small
// limit; uploads (S3 objects, Lambda ZIPs sent as base64, Lightsail bundles) keep
// Bun's default 128 MB ceiling.
app.use("/api/*", (c, next) =>
bodyLimit({
maxSize: requestBodyLimit(c.req.path),
onError: (c) => c.json({ error: "Request body is too large" }, 413),
})(c, next),
);
app.use("*", authMiddleware(authentication));
app.use("*", authRoutes(authentication));
app.get("/api/health", (c) => c.json({ status: "ok" }));
app.route("/api/eks", eks);
app.route("/api/ec2", ec2);
app.route("/api/secretsmanager", secretsmanager);
app.route("/api/clouds", clouds);
app.route("/api/msk", msk);
app.route("/api/msk-connect", mskConnect);
app.route("/api/ses", ses);
app.route("/api/elasticache", elasticache);
app.all("/api/*", (c) => c.json({ error: "Not found" }, 404));
// Serve static frontend files when public/ directory is present (production)
app.use("*", serveStatic({ root: "./public" }));
app.get("*", serveStatic({ path: "./public/index.html" }));
const port = Number(process.env.PORT ?? 4501);
export default { port, fetch: app.fetch };
function requestBodyLimit(path: string): number {
if (path.startsWith("/api/auth/")) return 16 * 1024;
if (
/^\/api\/clouds\/[^/]+\/services\/[^/]+\/resources(?:\/[^/]+(?:\/object|\/static-files|\/node-application)?)?$/.test(path)
) {
return 128 * 1024 * 1024;
}
return 10 * 1024 * 1024;
}
function isSensitiveCredentialRoute(path: string): boolean {
return (
path.startsWith("/api/secretsmanager") ||
/^\/api\/clouds\/[^/]+\/services\/kms\/resources\/[^/]+\/(?:encrypt|decrypt)$/.test(path)
);
}