fix: patch command injection, missing rate limit, Docker build, logger crash
- Sanitize session ID in control route to prevent command injection via unsanitized URL params interpolated into shell commands - Add mutationLimiter and structured logging to session control endpoint - Install python3/make/g++ in Dockerfile deps stage for better-sqlite3 native addon compilation - Handle missing public/ directory in Docker COPY with glob pattern - Guard pino-pretty transport against missing devDependency at runtime
This commit is contained in:
parent
0165173225
commit
c8f932344f
|
|
@ -4,6 +4,8 @@ WORKDIR /app
|
||||||
|
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
COPY package.json pnpm-lock.yaml ./
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
# better-sqlite3 requires native compilation tools
|
||||||
|
RUN apt-get update && apt-get install -y python3 make g++ --no-install-recommends && rm -rf /var/lib/apt/lists/*
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
|
|
@ -17,7 +19,8 @@ ENV NODE_ENV=production
|
||||||
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
||||||
COPY --from=build /app/.next/standalone ./
|
COPY --from=build /app/.next/standalone ./
|
||||||
COPY --from=build /app/.next/static ./.next/static
|
COPY --from=build /app/.next/static ./.next/static
|
||||||
COPY --from=build /app/public ./public
|
# Copy public directory if it exists (may not exist in all setups)
|
||||||
|
COPY --from=build /app/public* ./public/
|
||||||
USER nextjs
|
USER nextjs
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { requireRole } from '@/lib/auth'
|
import { requireRole } from '@/lib/auth'
|
||||||
import { runClawdbot } from '@/lib/command'
|
import { runClawdbot } from '@/lib/command'
|
||||||
import { db_helpers } from '@/lib/db'
|
import { db_helpers } from '@/lib/db'
|
||||||
|
import { mutationLimiter } from '@/lib/rate-limit'
|
||||||
|
import { logger } from '@/lib/logger'
|
||||||
|
|
||||||
|
// Only allow alphanumeric, hyphens, and underscores in session IDs
|
||||||
|
const SESSION_ID_RE = /^[a-zA-Z0-9_-]+$/
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
|
|
@ -10,10 +15,20 @@ export async function POST(
|
||||||
const auth = requireRole(request, 'operator')
|
const auth = requireRole(request, 'operator')
|
||||||
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
||||||
|
|
||||||
|
const rateCheck = mutationLimiter(request)
|
||||||
|
if (rateCheck) return rateCheck
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
const { action } = await request.json()
|
const { action } = await request.json()
|
||||||
|
|
||||||
|
if (!SESSION_ID_RE.test(id)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Invalid session ID format' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!['monitor', 'pause', 'terminate'].includes(action)) {
|
if (!['monitor', 'pause', 'terminate'].includes(action)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Invalid action. Must be: monitor, pause, terminate' },
|
{ error: 'Invalid action. Must be: monitor, pause, terminate' },
|
||||||
|
|
@ -53,6 +68,7 @@ export async function POST(
|
||||||
stdout: result.stdout.trim(),
|
stdout: result.stdout.trim(),
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
logger.error({ err: error }, 'Session control error')
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: error.message || 'Session control failed' },
|
{ error: error.message || 'Session control failed' },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,19 @@
|
||||||
import pino from 'pino'
|
import pino from 'pino'
|
||||||
|
|
||||||
|
function hasPinoPretty(): boolean {
|
||||||
|
try {
|
||||||
|
require.resolve('pino-pretty')
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const usePretty = process.env.NODE_ENV !== 'production' && hasPinoPretty()
|
||||||
|
|
||||||
export const logger = pino({
|
export const logger = pino({
|
||||||
level: process.env.LOG_LEVEL || 'info',
|
level: process.env.LOG_LEVEL || 'info',
|
||||||
...(process.env.NODE_ENV !== 'production' && {
|
...(usePretty && {
|
||||||
transport: {
|
transport: {
|
||||||
target: 'pino-pretty',
|
target: 'pino-pretty',
|
||||||
options: { colorize: true },
|
options: { colorize: true },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue