Security fixes: non-root user, signup rate limiting, differentiated CORS, persistent usage tracking

This commit is contained in:
OpenClaw 2026-02-14 17:04:55 +00:00
parent 6a38ba4adc
commit 73bb041513
5 changed files with 108 additions and 17 deletions

View file

@ -1,19 +1,31 @@
FROM node:22-bookworm-slim
# Install Chromium (works on ARM and x86)
# Install Chromium and dependencies as root
RUN apt-get update && apt-get install -y --no-install-recommends \
chromium fonts-liberation \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN groupadd --gid 1001 docfast \
&& useradd --uid 1001 --gid docfast --shell /bin/bash --create-home docfast
# Set environment variables
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist/ dist/
COPY public/ public/
# Create data directory and set ownership to docfast user
RUN mkdir -p /app/data && chown -R docfast:docfast /app
# Switch to non-root user
USER docfast
ENV PORT=3100
EXPOSE 3100
CMD ["node", "dist/index.js"]

19
Dockerfile.backup Normal file
View file

@ -0,0 +1,19 @@
FROM node:22-bookworm-slim
# Install Chromium (works on ARM and x86)
RUN apt-get update && apt-get install -y --no-install-recommends \
chromium fonts-liberation \
&& rm -rf /var/lib/apt/lists/*
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist/ dist/
COPY public/ public/
ENV PORT=3100
EXPOSE 3100
CMD ["node", "dist/index.js"]

View file

@ -22,19 +22,30 @@ loadKeys();
app.use(helmet({ crossOriginResourcePolicy: { policy: "cross-origin" } }));
// CORS — allow browser requests from the landing page
// Differentiated CORS middleware
app.use((req, res, next) => {
// Allow all origins — public API
res.setHeader("Access-Control-Allow-Origin", "*");
const isAuthBillingRoute = req.path.startsWith('/v1/signup') ||
req.path.startsWith('/v1/billing');
if (isAuthBillingRoute) {
// Auth/billing routes: restrict to docfast.dev
res.setHeader("Access-Control-Allow-Origin", "https://docfast.dev");
} else {
// Conversion API routes: allow all origins
res.setHeader("Access-Control-Allow-Origin", "*");
}
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key");
res.setHeader("Access-Control-Max-Age", "86400");
if (req.method === "OPTIONS") {
res.status(204).end();
return;
}
next();
});
// Raw body for Stripe webhook signature verification
app.use("/v1/billing/webhook", express.raw({ type: "application/json" }));
app.use(express.json({ limit: "2mb" }));

View file

@ -1,12 +1,9 @@
import { Request, Response, NextFunction } from "express";
import { isProKey } from "../services/keys.js";
import fs from "fs/promises";
import path from "path";
interface UsageRecord {
count: number;
monthKey: string;
}
const usage = new Map<string, UsageRecord>();
const USAGE_FILE = "/app/data/usage.json";
let usage = new Map<string, { count: number; monthKey: string }>();
const FREE_TIER_LIMIT = 100;
function getMonthKey(): string {
@ -14,11 +11,45 @@ function getMonthKey(): string {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
}
export function usageMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
// Load usage data from file on startup
async function loadUsageData(): Promise<void> {
try {
const data = await fs.readFile(USAGE_FILE, "utf8");
const usageObj = JSON.parse(data);
usage = new Map();
for (const [key, record] of Object.entries(usageObj)) {
usage.set(key, record as { count: number; monthKey: string });
}
console.log(`Loaded usage data for ${usage.size} keys`);
} catch (error) {
// File doesn't exist or invalid JSON - start fresh
console.log("No existing usage data found, starting fresh");
usage = new Map();
}
}
// Save usage data to file
async function saveUsageData(): Promise<void> {
try {
const usageObj: Record<string, { count: number; monthKey: string }> = {};
for (const [key, record] of usage) {
usageObj[key] = record;
}
// Ensure directory exists
await fs.mkdir(path.dirname(USAGE_FILE), { recursive: true });
await fs.writeFile(USAGE_FILE, JSON.stringify(usageObj, null, 2));
} catch (error) {
console.error("Failed to save usage data:", error);
}
}
// Initialize usage data loading
loadUsageData().catch(console.error);
export function usageMiddleware(req: any, res: any, next: any): void {
const key = req.headers.authorization?.slice(7) || "unknown";
const monthKey = getMonthKey();
@ -52,6 +83,9 @@ function trackUsage(key: string, monthKey: string): void {
} else {
record.count++;
}
// Save to file after each update (simple approach)
saveUsageData().catch(console.error);
}
export function getUsageStats(): Record<string, { count: number; month: string }> {

View file

@ -1,10 +1,25 @@
import { Router, Request, Response } from "express";
import rateLimit from "express-rate-limit";
import { createFreeKey } from "../services/keys.js";
const router = Router();
// Rate limiting for signup - 5 signups per IP per hour
const signupLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 signups per IP per hour
message: {
error: "Too many signup attempts. Please try again in 1 hour.",
retryAfter: "1 hour"
},
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: false,
skipFailedRequests: false,
});
// Self-service free API key signup
router.post("/free", (req: Request, res: Response) => {
router.post("/free", signupLimiter, (req: Request, res: Response) => {
const { email } = req.body;
if (!email || typeof email !== "string") {