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:
Nyk 2026-02-27 21:57:50 +07:00
parent 0165173225
commit c8f932344f
3 changed files with 32 additions and 2 deletions

View File

@ -4,6 +4,8 @@ WORKDIR /app
FROM base AS deps
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
FROM base AS build
@ -17,7 +19,8 @@ ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
COPY --from=build /app/.next/standalone ./
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
EXPOSE 3000
CMD ["node", "server.js"]

View File

@ -2,6 +2,11 @@ import { NextRequest, NextResponse } from 'next/server'
import { requireRole } from '@/lib/auth'
import { runClawdbot } from '@/lib/command'
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(
request: NextRequest,
@ -10,10 +15,20 @@ export async function POST(
const auth = requireRole(request, 'operator')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const rateCheck = mutationLimiter(request)
if (rateCheck) return rateCheck
try {
const { id } = await params
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)) {
return NextResponse.json(
{ error: 'Invalid action. Must be: monitor, pause, terminate' },
@ -53,6 +68,7 @@ export async function POST(
stdout: result.stdout.trim(),
})
} catch (error: any) {
logger.error({ err: error }, 'Session control error')
return NextResponse.json(
{ error: error.message || 'Session control failed' },
{ status: 500 }

View File

@ -1,8 +1,19 @@
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({
level: process.env.LOG_LEVEL || 'info',
...(process.env.NODE_ENV !== 'production' && {
...(usePretty && {
transport: {
target: 'pino-pretty',
options: { colorize: true },