[object Object]

← back to Wallco Ai

_evidence: snapshot bubbe RCE routes (hooks + upload-bubbe) before de-root 20260530

a37f6a3c6b8470503972001426bd0951bdeac365 · 2026-05-30 08:23:24 -0700 · Steve Abrams

Pre-removal evidence of the RCE surface (PID 3771, port 3011, root-owned)
that was the identified entry vector for the pakchoi UID-0 backdoor.

- app/api/hooks/[hookName]/route.ts (309 lines) — child_process exec()
  as root, accessible via https://www.bubbe.ai/api/hooks/<hookName>
- app/api/upload-bubbe/route.ts (47 lines) — formdata image upload
  written to disk as root, fixed path /public/bubbe.png

Process killed (TERM, already gone by execution time), pakchoi UID-0
entries surgically removed from /etc/passwd /etc/shadow /etc/group,
sys-health.{service,timer} disabled and quarantined on prod at
/root/quarantine/sys-health-20260530/.

DTD verdicts: bubbe=A (1-1 tie, Codex argument weighted), pakchoi=B
(unanimous, archive-then-remove), sys-health=A (unanimous, disable+
quarantine). qwen errored on all 3 panels (Mac1 ollama load contention).

Files touched

Diff

commit a37f6a3c6b8470503972001426bd0951bdeac365
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat May 30 08:23:24 2026 -0700

    _evidence: snapshot bubbe RCE routes (hooks + upload-bubbe) before de-root 20260530
    
    Pre-removal evidence of the RCE surface (PID 3771, port 3011, root-owned)
    that was the identified entry vector for the pakchoi UID-0 backdoor.
    
    - app/api/hooks/[hookName]/route.ts (309 lines) — child_process exec()
      as root, accessible via https://www.bubbe.ai/api/hooks/<hookName>
    - app/api/upload-bubbe/route.ts (47 lines) — formdata image upload
      written to disk as root, fixed path /public/bubbe.png
    
    Process killed (TERM, already gone by execution time), pakchoi UID-0
    entries surgically removed from /etc/passwd /etc/shadow /etc/group,
    sys-health.{service,timer} disabled and quarantined on prod at
    /root/quarantine/sys-health-20260530/.
    
    DTD verdicts: bubbe=A (1-1 tie, Codex argument weighted), pakchoi=B
    (unanimous, archive-then-remove), sys-health=A (unanimous, disable+
    quarantine). qwen errored on all 3 panels (Mac1 ollama load contention).
---
 _evidence/bubbe-deroot-20260530/hooks-route.ts     | 310 +++++++++++++++++++++
 .../bubbe-deroot-20260530/upload-bubbe-route.ts    |  47 ++++
 2 files changed, 357 insertions(+)

diff --git a/_evidence/bubbe-deroot-20260530/hooks-route.ts b/_evidence/bubbe-deroot-20260530/hooks-route.ts
new file mode 100644
index 0000000..de792c4
--- /dev/null
+++ b/_evidence/bubbe-deroot-20260530/hooks-route.ts
@@ -0,0 +1,310 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { exec } from 'child_process'
+import { promisify } from 'util'
+import { updateStat } from '../../stats/route'
+
+const execAsync = promisify(exec)
+
+interface HookResult {
+  success: boolean
+  output?: string
+  error?: string
+  executionTime: number
+  hookName: string
+  action?: string
+}
+
+// Available system hooks
+const AVAILABLE_HOOKS = {
+  'service-health-check': {
+    description: 'Check the health of all services and processes',
+    actions: ['check', 'fix'],
+    executable: true
+  },
+  'agent-health-monitor': {
+    description: 'Monitor the health of all DW agents',
+    actions: ['check'],
+    executable: true
+  },
+  'cache-cleanup': {
+    description: 'Clean up system cache and temporary files',
+    actions: ['clean', 'deep-clean'],
+    executable: true
+  },
+  'log-rotation': {
+    description: 'Rotate and compress log files',
+    actions: ['rotate', 'compress'],
+    executable: true
+  },
+  'backup-data': {
+    description: 'Backup user data and configurations',
+    actions: ['backup', 'restore'],
+    executable: true
+  },
+  'restart-services': {
+    description: 'Restart specific services or all services',
+    actions: ['restart', 'reload'],
+    executable: true
+  }
+}
+
+// Mock hook execution for safety - replace with actual hook scripts
+async function executeHook(hookName: string, action?: string): Promise<HookResult> {
+  const startTime = Date.now()
+  
+  try {
+    let output = ''
+    let success = true
+
+    switch (hookName) {
+      case 'service-health-check':
+        if (action === 'check') {
+          // Check PM2 processes
+          try {
+            const { stdout } = await execAsync('pm2 status')
+            output = `✅ PM2 Status Check:\n${stdout}\n`
+            
+            // Check disk usage
+            const { stdout: diskUsage } = await execAsync('df -h /')
+            output += `✅ Disk Usage:\n${diskUsage}\n`
+            
+            // Check memory usage
+            const { stdout: memInfo } = await execAsync('free -h')
+            output += `✅ Memory Usage:\n${memInfo}\n`
+          } catch (error) {
+            success = false
+            output = `❌ Health check failed: ${error instanceof Error ? error.message : String(error)}`
+          }
+        } else if (action === 'fix') {
+          try {
+            // Attempt to fix common issues
+            await execAsync('pm2 restart bubbe-ai')
+            output = '✅ Restarted bubbe-ai process\n'
+            
+            // Clear npm cache
+            await execAsync('npm cache clean --force')
+            output += '✅ Cleared npm cache\n'
+            
+            // Clear Next.js cache
+            await execAsync('rm -rf .next')
+            output += '✅ Cleared Next.js cache\n'
+          } catch (error) {
+            success = false
+            output = `❌ Auto-fix failed: ${error instanceof Error ? error.message : String(error)}`
+          }
+        }
+        break
+
+      case 'agent-health-monitor':
+        try {
+          const { stdout } = await execAsync('pm2 list | grep -E "(online|errored|stopped)"')
+          const lines = stdout.split('\n').filter(line => line.trim())
+          
+          let onlineCount = 0
+          let errorCount = 0
+          let stoppedCount = 0
+          
+          lines.forEach(line => {
+            if (line.includes('online')) onlineCount++
+            else if (line.includes('errored')) errorCount++
+            else if (line.includes('stopped')) stoppedCount++
+          })
+          
+          output = `🤖 Agent Health Summary:
+✅ Online: ${onlineCount}
+⚠️ Errored: ${errorCount} 
+🔴 Stopped: ${stoppedCount}
+
+${stdout}`
+        } catch (error) {
+          success = false
+          output = `❌ Agent monitor failed: ${error instanceof Error ? error.message : String(error)}`
+        }
+        break
+
+      case 'cache-cleanup':
+        try {
+          if (action === 'deep-clean') {
+            await execAsync('npm run purge-cache')
+            await execAsync('rm -rf node_modules/.cache')
+            await execAsync('rm -rf /tmp/npm-*')
+            output = '✅ Deep cache cleanup completed'
+          } else {
+            await execAsync('npm cache clean --force')
+            output = '✅ Basic cache cleanup completed'
+          }
+        } catch (error) {
+          success = false
+          output = `❌ Cache cleanup failed: ${error instanceof Error ? error.message : String(error)}`
+        }
+        break
+
+      case 'log-rotation':
+        try {
+          // Rotate PM2 logs
+          await execAsync('pm2 flush')
+          output = '✅ PM2 logs rotated\n'
+          
+          // Compress old log files (mock)
+          output += '✅ Old logs compressed\n'
+          output += '✅ Log rotation completed'
+        } catch (error) {
+          success = false
+          output = `❌ Log rotation failed: ${error instanceof Error ? error.message : String(error)}`
+        }
+        break
+
+      case 'backup-data':
+        try {
+          if (action === 'backup') {
+            // Mock backup process
+            output = '✅ User memories backed up\n'
+            output += '✅ Configuration files backed up\n'
+            output += '✅ Database backed up\n'
+            output += `✅ Backup completed: backup_${Date.now()}.tar.gz`
+          } else if (action === 'restore') {
+            output = '⚠️ Restore operation requires manual confirmation'
+            success = false
+          }
+        } catch (error) {
+          success = false
+          output = `❌ Backup operation failed: ${error instanceof Error ? error.message : String(error)}`
+        }
+        break
+
+      case 'restart-services':
+        try {
+          if (action === 'restart') {
+            await execAsync('pm2 restart bubbe-ai')
+            output = '✅ Services restarted successfully'
+          } else if (action === 'reload') {
+            await execAsync('pm2 reload bubbe-ai')
+            output = '✅ Services reloaded successfully'
+          }
+        } catch (error) {
+          success = false
+          output = `❌ Service restart failed: ${error instanceof Error ? error.message : String(error)}`
+        }
+        break
+
+      default:
+        success = false
+        output = `❌ Unknown hook: ${hookName}`
+        break
+    }
+
+    const executionTime = Date.now() - startTime
+    
+    // Update stats
+    if (success) {
+      updateStat('tasksCompleted', 1)
+    } else {
+      updateStat('errorsToday', 1)
+    }
+
+    return {
+      success,
+      output,
+      executionTime,
+      hookName,
+      action
+    }
+  } catch (error) {
+    const executionTime = Date.now() - startTime
+    updateStat('errorsToday', 1)
+    
+    return {
+      success: false,
+      error: error instanceof Error ? error.message : String(error),
+      executionTime,
+      hookName,
+      action
+    }
+  }
+}
+
+export async function POST(request: NextRequest, { params }: { params: Promise<{ hookName: string }> }) {
+  try {
+    const { hookName } = await params
+    const body = await request.json().catch(() => ({}))
+    const { action } = body
+
+    // Validate hook name
+    if (!(hookName in AVAILABLE_HOOKS)) {
+      return NextResponse.json({
+        error: 'Unknown hook',
+        hookName,
+        availableHooks: Object.keys(AVAILABLE_HOOKS)
+      }, { status: 400 })
+    }
+
+    const hookConfig = AVAILABLE_HOOKS[hookName as keyof typeof AVAILABLE_HOOKS]
+
+    // Validate action if provided
+    if (action && !hookConfig.actions.includes(action)) {
+      return NextResponse.json({
+        error: 'Invalid action for hook',
+        hookName,
+        action,
+        validActions: hookConfig.actions
+      }, { status: 400 })
+    }
+
+    console.log(`[HOOKS] Executing hook: ${hookName}${action ? ` with action: ${action}` : ''}`)
+
+    const result = await executeHook(hookName, action)
+
+    console.log(`[HOOKS] Hook ${hookName} completed in ${result.executionTime}ms, success: ${result.success}`)
+
+    return NextResponse.json(result)
+  } catch (error) {
+    console.error('[HOOKS] Error executing hook:', error)
+    return NextResponse.json({
+      error: 'Hook execution failed',
+      details: error instanceof Error ? error.message : String(error)
+    }, { status: 500 })
+  }
+}
+
+export async function GET(request: NextRequest, { params }: { params: Promise<{ hookName: string }> }) {
+  try {
+    const { hookName } = await params
+
+    if (hookName === 'list') {
+      // Return list of all available hooks
+      return NextResponse.json({
+        availableHooks: Object.entries(AVAILABLE_HOOKS).map(([name, config]) => ({
+          name,
+          description: config.description,
+          actions: config.actions,
+          executable: config.executable
+        }))
+      })
+    }
+
+    // Return information about specific hook
+    if (!(hookName in AVAILABLE_HOOKS)) {
+      return NextResponse.json({
+        error: 'Unknown hook',
+        hookName,
+        availableHooks: Object.keys(AVAILABLE_HOOKS)
+      }, { status: 404 })
+    }
+
+    const hookConfig = AVAILABLE_HOOKS[hookName as keyof typeof AVAILABLE_HOOKS]
+    
+    return NextResponse.json({
+      name: hookName,
+      description: hookConfig.description,
+      actions: hookConfig.actions,
+      executable: hookConfig.executable,
+      usage: `POST to /api/hooks/${hookName} with optional { "action": "..." } body`
+    })
+  } catch (error) {
+    console.error('[HOOKS] Error getting hook info:', error)
+    return NextResponse.json({
+      error: 'Failed to get hook information',
+      details: error instanceof Error ? error.message : String(error)
+    }, { status: 500 })
+  }
+}
\ No newline at end of file
diff --git a/_evidence/bubbe-deroot-20260530/upload-bubbe-route.ts b/_evidence/bubbe-deroot-20260530/upload-bubbe-route.ts
new file mode 100644
index 0000000..8522d06
--- /dev/null
+++ b/_evidence/bubbe-deroot-20260530/upload-bubbe-route.ts
@@ -0,0 +1,47 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { writeFile } from 'fs/promises'
+import { join } from 'path'
+
+export async function POST(request: NextRequest) {
+  try {
+    const formData = await request.formData()
+    const file = formData.get('image') as File
+
+    if (!file) {
+      return NextResponse.json(
+        { error: 'No file provided' },
+        { status: 400 }
+      )
+    }
+
+    // Validate file type
+    if (!file.type.startsWith('image/')) {
+      return NextResponse.json(
+        { error: 'File must be an image' },
+        { status: 400 }
+      )
+    }
+
+    // Convert file to buffer
+    const bytes = await file.arrayBuffer()
+    const buffer = Buffer.from(bytes)
+
+    // Save to public/bubbe.png
+    const path = join(process.cwd(), 'public', 'bubbe.png')
+    await writeFile(path, buffer)
+
+    console.log('[UPLOAD] Bubbe image uploaded successfully:', path)
+
+    return NextResponse.json({
+      success: true,
+      message: 'Bubbe image uploaded successfully'
+    })
+
+  } catch (error) {
+    console.error('[UPLOAD] Error uploading Bubbe image:', error)
+    return NextResponse.json(
+      { error: 'Upload failed: ' + (error as Error).message },
+      { status: 500 }
+    )
+  }
+}

← 0ed2c60 backfill-rooms-cron.sh: atomic designs.json write + catastro  ·  back to Wallco Ai  ·  backfill-rooms-cron: indent=2 + byte-size guard (95%) to sto 1b9059d →