← 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
A _evidence/bubbe-deroot-20260530/hooks-route.tsA _evidence/bubbe-deroot-20260530/upload-bubbe-route.ts
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 →