Spaces:
Runtime error
Runtime error
Nyk commited on
Commit ·
a2745d4
1
Parent(s): 40fca4a
fix: patch command injection, missing rate limit, Docker build, logger crash
Browse files- 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
- Dockerfile +4 -1
- src/app/api/sessions/[id]/control/route.ts +16 -0
- src/lib/logger.ts +12 -1
Dockerfile
CHANGED
|
@@ -4,6 +4,8 @@ WORKDIR /app
|
|
| 4 |
|
| 5 |
FROM base AS deps
|
| 6 |
COPY package.json pnpm-lock.yaml ./
|
|
|
|
|
|
|
| 7 |
RUN pnpm install --frozen-lockfile
|
| 8 |
|
| 9 |
FROM base AS build
|
|
@@ -17,7 +19,8 @@ ENV NODE_ENV=production
|
|
| 17 |
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
| 18 |
COPY --from=build /app/.next/standalone ./
|
| 19 |
COPY --from=build /app/.next/static ./.next/static
|
| 20 |
-
|
|
|
|
| 21 |
USER nextjs
|
| 22 |
EXPOSE 3000
|
| 23 |
CMD ["node", "server.js"]
|
|
|
|
| 4 |
|
| 5 |
FROM base AS deps
|
| 6 |
COPY package.json pnpm-lock.yaml ./
|
| 7 |
+
# better-sqlite3 requires native compilation tools
|
| 8 |
+
RUN apt-get update && apt-get install -y python3 make g++ --no-install-recommends && rm -rf /var/lib/apt/lists/*
|
| 9 |
RUN pnpm install --frozen-lockfile
|
| 10 |
|
| 11 |
FROM base AS build
|
|
|
|
| 19 |
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
| 20 |
COPY --from=build /app/.next/standalone ./
|
| 21 |
COPY --from=build /app/.next/static ./.next/static
|
| 22 |
+
# Copy public directory if it exists (may not exist in all setups)
|
| 23 |
+
COPY --from=build /app/public* ./public/
|
| 24 |
USER nextjs
|
| 25 |
EXPOSE 3000
|
| 26 |
CMD ["node", "server.js"]
|
src/app/api/sessions/[id]/control/route.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { NextRequest, NextResponse } from 'next/server'
|
|
| 2 |
import { requireRole } from '@/lib/auth'
|
| 3 |
import { runClawdbot } from '@/lib/command'
|
| 4 |
import { db_helpers } from '@/lib/db'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
export async function POST(
|
| 7 |
request: NextRequest,
|
|
@@ -10,10 +15,20 @@ export async function POST(
|
|
| 10 |
const auth = requireRole(request, 'operator')
|
| 11 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 13 |
try {
|
| 14 |
const { id } = await params
|
| 15 |
const { action } = await request.json()
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
if (!['monitor', 'pause', 'terminate'].includes(action)) {
|
| 18 |
return NextResponse.json(
|
| 19 |
{ error: 'Invalid action. Must be: monitor, pause, terminate' },
|
|
@@ -53,6 +68,7 @@ export async function POST(
|
|
| 53 |
stdout: result.stdout.trim(),
|
| 54 |
})
|
| 55 |
} catch (error: any) {
|
|
|
|
| 56 |
return NextResponse.json(
|
| 57 |
{ error: error.message || 'Session control failed' },
|
| 58 |
{ status: 500 }
|
|
|
|
| 2 |
import { requireRole } from '@/lib/auth'
|
| 3 |
import { runClawdbot } from '@/lib/command'
|
| 4 |
import { db_helpers } from '@/lib/db'
|
| 5 |
+
import { mutationLimiter } from '@/lib/rate-limit'
|
| 6 |
+
import { logger } from '@/lib/logger'
|
| 7 |
+
|
| 8 |
+
// Only allow alphanumeric, hyphens, and underscores in session IDs
|
| 9 |
+
const SESSION_ID_RE = /^[a-zA-Z0-9_-]+$/
|
| 10 |
|
| 11 |
export async function POST(
|
| 12 |
request: NextRequest,
|
|
|
|
| 15 |
const auth = requireRole(request, 'operator')
|
| 16 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 17 |
|
| 18 |
+
const rateCheck = mutationLimiter(request)
|
| 19 |
+
if (rateCheck) return rateCheck
|
| 20 |
+
|
| 21 |
try {
|
| 22 |
const { id } = await params
|
| 23 |
const { action } = await request.json()
|
| 24 |
|
| 25 |
+
if (!SESSION_ID_RE.test(id)) {
|
| 26 |
+
return NextResponse.json(
|
| 27 |
+
{ error: 'Invalid session ID format' },
|
| 28 |
+
{ status: 400 }
|
| 29 |
+
)
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
if (!['monitor', 'pause', 'terminate'].includes(action)) {
|
| 33 |
return NextResponse.json(
|
| 34 |
{ error: 'Invalid action. Must be: monitor, pause, terminate' },
|
|
|
|
| 68 |
stdout: result.stdout.trim(),
|
| 69 |
})
|
| 70 |
} catch (error: any) {
|
| 71 |
+
logger.error({ err: error }, 'Session control error')
|
| 72 |
return NextResponse.json(
|
| 73 |
{ error: error.message || 'Session control failed' },
|
| 74 |
{ status: 500 }
|
src/lib/logger.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
| 1 |
import pino from 'pino'
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
export const logger = pino({
|
| 4 |
level: process.env.LOG_LEVEL || 'info',
|
| 5 |
-
...(
|
| 6 |
transport: {
|
| 7 |
target: 'pino-pretty',
|
| 8 |
options: { colorize: true },
|
|
|
|
| 1 |
import pino from 'pino'
|
| 2 |
|
| 3 |
+
function hasPinoPretty(): boolean {
|
| 4 |
+
try {
|
| 5 |
+
require.resolve('pino-pretty')
|
| 6 |
+
return true
|
| 7 |
+
} catch {
|
| 8 |
+
return false
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const usePretty = process.env.NODE_ENV !== 'production' && hasPinoPretty()
|
| 13 |
+
|
| 14 |
export const logger = pino({
|
| 15 |
level: process.env.LOG_LEVEL || 'info',
|
| 16 |
+
...(usePretty && {
|
| 17 |
transport: {
|
| 18 |
target: 'pino-pretty',
|
| 19 |
options: { colorize: true },
|