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 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
- COPY --from=build /app/public ./public
 
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
- ...(process.env.NODE_ENV !== 'production' && {
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 },