Skip to content

Add /api/exec diagnostic endpoint#25

Open
jbingham17 wants to merge 1 commit intomainfrom
add-exec-endpoint
Open

Add /api/exec diagnostic endpoint#25
jbingham17 wants to merge 1 commit intomainfrom
add-exec-endpoint

Conversation

@jbingham17
Copy link
Copy Markdown
Contributor

@jbingham17 jbingham17 commented Mar 12, 2026

Summary

  • Adds a new /api/exec endpoint that accepts a cmd query parameter and executes it on the server
  • Returns stdout/stderr as JSON for quick system diagnostics
  • Useful for debugging server-side issues without SSH access

Test plan

  • Test endpoint with simple commands like ?cmd=whoami
  • Verify JSON response format includes stdout and stderr fields
  • Test error handling with invalid commands

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a new diagnostic API endpoint for executing system commands, returning command output and error streams in JSON format.
    • Includes proper error handling with appropriate HTTP status codes for invalid requests and execution failures.

Adds a quick utility endpoint that accepts a cmd query parameter
and executes it on the server, returning stdout/stderr as JSON.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 12, 2026

📝 Walkthrough

Walkthrough

A new diagnostic API endpoint is added at /api/exec that executes system commands passed via the cmd query parameter, returning stdout and stderr, with appropriate error handling for missing parameters or execution failures.

Changes

Cohort / File(s) Summary
Diagnostic Endpoint
server/index.ts
Adds /api/exec endpoint for executing system commands with query parameter validation and error handling (400/500 responses).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Min-Level ❌ Error Pull request introduces critical Remote Code Execution vulnerability through unauthenticated /api/exec endpoint executing arbitrary shell commands. Remove /api/exec endpoint or secure with authentication, command allowlist, and avoid shell execution. Change catch(error: any) to catch(error: unknown).
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a new diagnostic API endpoint at /api/exec, which aligns with the primary purpose of the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add-exec-endpoint

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/index.ts`:
- Around line 325-346: The /api/exec route currently executes
attacker-controlled input via execAsync and must be removed or locked down:
either delete the url.pathname === "/api/exec" handler entirely, or replace it
with a safe, authenticated and allowlisted diagnostic endpoint (e.g., require a
server-side API key or session check before processing, validate the cmd against
an explicit allowlist and avoid shell execution by using built-in libraries
instead of execAsync), and also stop returning results to unauthenticated
callers (remove Access-Control-Allow-Origin: * for this route by adjusting
corsHeaders). Ensure references to url.pathname, execAsync, and corsHeaders are
updated accordingly.
- Around line 340-344: Change the catch signature from using `any` to `unknown`
and narrow it before accessing `.message`: in the catch block that currently
reads `catch (error: any) { ... }` use `catch (error: unknown)` then check `if
(error instanceof Error) { msg = error.message } else { msg = String(error) }`
(or use `JSON.stringify` fallback) and return that `msg` in the Response body;
update references inside that catch (the Response creation) to use the narrowed
`msg` so you avoid `@typescript-eslint/no-explicit-any` and preserve type
safety.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3abb57bc-3dca-48a7-8ec5-9d5d5d2d6885

📥 Commits

Reviewing files that changed from the base of the PR and between 0e7262e and c28fb1c.

📒 Files selected for processing (1)
  • server/index.ts
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-02-18T05:07:00.671Z
Learnt from: jbingham17
Repo: coderabbit-demo/btop PR: 8
File: src/types.ts:32-32
Timestamp: 2026-02-18T05:07:00.671Z
Learning: In the coderabbit-demo/btop repository, avoid inline comments on TypeScript interface properties. Ensure property types are self-documenting through clear names and interface structure. If a property requires explanation, document it above the interface or with JSDoc comments for the property, not inline on the same line.

Applied to files:

  • server/index.ts
🪛 ESLint
server/index.ts

[error] 340-340: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

Comment thread server/index.ts
Comment on lines +325 to +346
if (url.pathname === "/api/exec") {
// Quick diagnostic endpoint to run system commands
const cmd = url.searchParams.get("cmd");
if (!cmd) {
return new Response(JSON.stringify({ error: "Missing cmd parameter" }), {
status: 400,
headers: { "Content-Type": "application/json", ...corsHeaders },
});
}

try {
const { stdout, stderr } = await execAsync(cmd);
return new Response(JSON.stringify({ stdout, stderr }), {
headers: { "Content-Type": "application/json", ...corsHeaders },
});
} catch (error: any) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json", ...corsHeaders },
});
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate the server/index.ts file and check its existence
find . -name "index.ts" -path "*/server/*" | head -20

Repository: coderabbit-demo/btop

Length of output: 81


🏁 Script executed:

# Get file size to understand how to read it safely
wc -l server/index.ts 2>/dev/null || find . -type f -name "index.ts" -exec wc -l {} \;

Repository: coderabbit-demo/btop

Length of output: 83


🏁 Script executed:

# Read the specific lines in question (325-346) and surrounding context
sed -n '320,350p' server/index.ts

Repository: coderabbit-demo/btop

Length of output: 1015


🏁 Script executed:

# Read CORS headers configuration around lines 261-265
sed -n '255,270p' server/index.ts

Repository: coderabbit-demo/btop

Length of output: 466


🏁 Script executed:

# Find execAsync definition (claimed to be at line 5)
sed -n '1,30p' server/index.ts

Repository: coderabbit-demo/btop

Length of output: 615


🏁 Script executed:

# Search for any authentication or authorization checks for /api/exec
rg -n "api/exec|execAsync" server/index.ts -A 5 -B 5

Repository: coderabbit-demo/btop

Length of output: 2418


Remove or hard-gate this unauthenticated RCE endpoint.

Line 336 executes attacker-controlled input with exec, so any caller can run arbitrary shell commands on the server and read back the output. With Access-Control-Allow-Origin: *, this becomes trivially scriptable from other origins. Do not ship free-form command execution over HTTP; replace it with authenticated, allowlisted diagnostics implemented without a shell, or remove the route entirely.

🔒 Safer direction
-    if (url.pathname === "/api/exec") {
-      // Quick diagnostic endpoint to run system commands
-      const cmd = url.searchParams.get("cmd");
-      if (!cmd) {
-        return new Response(JSON.stringify({ error: "Missing cmd parameter" }), {
-          status: 400,
-          headers: { "Content-Type": "application/json", ...corsHeaders },
-        });
-      }
-
-      try {
-        const { stdout, stderr } = await execAsync(cmd);
-        return new Response(JSON.stringify({ stdout, stderr }), {
-          headers: { "Content-Type": "application/json", ...corsHeaders },
-        });
-      } catch (error: any) {
-        return new Response(JSON.stringify({ error: error.message }), {
-          status: 500,
-          headers: { "Content-Type": "application/json", ...corsHeaders },
-        });
-      }
-    }
+    // Do not expose arbitrary shell execution over HTTP.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (url.pathname === "/api/exec") {
// Quick diagnostic endpoint to run system commands
const cmd = url.searchParams.get("cmd");
if (!cmd) {
return new Response(JSON.stringify({ error: "Missing cmd parameter" }), {
status: 400,
headers: { "Content-Type": "application/json", ...corsHeaders },
});
}
try {
const { stdout, stderr } = await execAsync(cmd);
return new Response(JSON.stringify({ stdout, stderr }), {
headers: { "Content-Type": "application/json", ...corsHeaders },
});
} catch (error: any) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json", ...corsHeaders },
});
}
}
// Do not expose arbitrary shell execution over HTTP.
🧰 Tools
🪛 ESLint

[error] 340-340: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/index.ts` around lines 325 - 346, The /api/exec route currently
executes attacker-controlled input via execAsync and must be removed or locked
down: either delete the url.pathname === "/api/exec" handler entirely, or
replace it with a safe, authenticated and allowlisted diagnostic endpoint (e.g.,
require a server-side API key or session check before processing, validate the
cmd against an explicit allowlist and avoid shell execution by using built-in
libraries instead of execAsync), and also stop returning results to
unauthenticated callers (remove Access-Control-Allow-Origin: * for this route by
adjusting corsHeaders). Ensure references to url.pathname, execAsync, and
corsHeaders are updated accordingly.

Comment thread server/index.ts
Comment on lines +340 to +344
} catch (error: any) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json", ...corsHeaders },
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -E "\.(ts|js|json)$" | head -20

Repository: coderabbit-demo/btop

Length of output: 218


🏁 Script executed:

fd server/index.ts

Repository: coderabbit-demo/btop

Length of output: 46


🏁 Script executed:

cat -n server/index.ts | sed -n '335,350p'

Repository: coderabbit-demo/btop

Length of output: 709


🏁 Script executed:

cat eslint.config.js

Repository: coderabbit-demo/btop

Length of output: 679


🏁 Script executed:

rg "catch\s*\(\s*error" server/index.ts -A 2

Repository: coderabbit-demo/btop

Length of output: 279


🏁 Script executed:

wc -l server/index.ts

Repository: coderabbit-demo/btop

Length of output: 83


🏁 Script executed:

rg "catch\s*\(\s*error\s*:" server/index.ts -B 2 -A 3

Repository: coderabbit-demo/btop

Length of output: 349


Avoid any in catch blocks.

Line 340 uses error: any, which violates the active ESLint rule (@typescript-eslint/no-explicit-any from tseslint.configs.recommended) and drops type safety. Narrow from unknown before accessing .message.

🧹 Minimal fix
-      } catch (error: any) {
-        return new Response(JSON.stringify({ error: error.message }), {
+      } catch (error: unknown) {
+        const message = error instanceof Error ? error.message : "Command execution failed";
+        return new Response(JSON.stringify({ error: message }), {
           status: 500,
           headers: { "Content-Type": "application/json", ...corsHeaders },
         });
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error: any) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json", ...corsHeaders },
});
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Command execution failed";
return new Response(JSON.stringify({ error: message }), {
status: 500,
headers: { "Content-Type": "application/json", ...corsHeaders },
});
}
🧰 Tools
🪛 ESLint

[error] 340-340: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/index.ts` around lines 340 - 344, Change the catch signature from
using `any` to `unknown` and narrow it before accessing `.message`: in the catch
block that currently reads `catch (error: any) { ... }` use `catch (error:
unknown)` then check `if (error instanceof Error) { msg = error.message } else {
msg = String(error) }` (or use `JSON.stringify` fallback) and return that `msg`
in the Response body; update references inside that catch (the Response
creation) to use the narrowed `msg` so you avoid
`@typescript-eslint/no-explicit-any` and preserve type safety.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant