Introduction
The server/index.js file in skill-cabinet handles a local web service for managing skills, but a critical flaw at line 131 created a severe security risk. The app.delete("/api/skills/:id") endpoint was completely exposed—no authentication, no authorization, no origin validation. Any process with network access to port 3781 could delete arbitrary skills from the filesystem simply by knowing or guessing a skill ID.
This vulnerability is particularly dangerous because it affects a destructive operation. While the service runs on localhost, browser-based attacks (like malicious JavaScript on a webpage) could potentially exploit this endpoint through cross-origin requests, turning a "local-only" service into a remotely exploitable target.
The Vulnerability Explained
The vulnerable code was deceptively simple—a straightforward Express route handler with no security controls:
app.delete("/api/skills/:id", (req, res) => {
try {
const result = deleteIds([req.params.id]);
const status = result.deleted.length ? 200 : 400;
res.status(status).json(result);
} catch (err) {
res.status(err.status || 500).json({ error: err.message });
}
});
Notice what's missing: there's no middleware checking who is making this request. The deleteIds() function directly receives the req.params.id and proceeds to remove the corresponding skill from the local filesystem.
The Attack Scenario
An attacker could exploit this in multiple ways:
- Direct HTTP Request: Simply run
curl -X DELETE http://127.0.0.1:3781/api/skills/my-important-skillfrom any process on the machine - Browser-Based Attack: A malicious webpage could execute JavaScript that sends DELETE requests to localhost:3781, potentially deleting all skills if the attacker can enumerate or guess skill IDs
- Malicious Browser Extension: Extensions have access to make requests to localhost, bypassing same-origin restrictions
The real-world impact here is data loss. Skills stored on the filesystem would be permanently deleted, and without backup mechanisms, users could lose important configurations or custom skills they've created.
The Fix
The fix takes a defense-in-depth approach with two key changes:
1. Remove the Unprotected Endpoint Entirely
The standalone DELETE /api/skills/:id endpoint was removed completely. Instead, delete operations are consolidated into the existing POST /api/skills/delete endpoint, which can handle single or batch deletions.
2. Add Loopback Origin Validation Middleware
A new middleware function validates that requests originate from localhost:
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
export function isLoopbackOrigin(origin) {
if (!origin) return false;
try {
return LOOPBACK_HOSTS.has(new URL(origin).hostname);
} catch {
return false;
}
}
function requireLoopbackOrigin(req, res, next) {
if (!isLoopbackOrigin(req.headers.origin)) {
res.status(403).json({ error: "Cross-origin request blocked" });
return;
}
next();
}
Before vs After
Before (vulnerable):
app.delete("/api/skills/:id", (req, res) => {
// No authentication - anyone can delete!
const result = deleteIds([req.params.id]);
// ...
});
app.post("/api/skills/delete", (req, res) => {
// Also no authentication
// ...
});
After (secured):
// DELETE endpoint removed entirely
app.post("/api/skills/delete", requireLoopbackOrigin, (req, res) => {
// Now requires loopback origin validation
// ...
});
app.post("/api/skills/quarantine", requireLoopbackOrigin, (req, res) => {
// Quarantine also protected
// ...
});
The fix also updates bin/skill-cabinet.js to properly export and call a start() function, ensuring the server initialization is controlled:
// Before
await import("../server/index.js");
// After
const { start } = await import("../server/index.js");
start();
Prevention & Best Practices
1. Apply Authentication Middleware Globally
For local services, consider applying origin validation to all state-changing endpoints by default:
app.use('/api', requireLoopbackOrigin);
2. Follow the Principle of Least Privilege
Destructive operations (DELETE, bulk modifications) should have stricter access controls than read operations.
3. Use Allowlists, Not Blocklists
The fix uses a Set of known-safe loopback addresses rather than trying to block malicious origins. This is more secure because it defaults to denial.
4. Consider Request Source Validation
For localhost services, validate both the Origin header and consider additional checks like checking the remote address:
const isLocal = req.ip === '127.0.0.1' || req.ip === '::1';
5. Audit All Route Handlers
Review every Express route to ensure appropriate middleware is applied, especially for POST, PUT, DELETE, and PATCH methods.
Key Takeaways
- The
DELETE /api/skills/:idendpoint at line 131 was completely unprotected, allowing filesystem manipulation without authentication - Removing unnecessary endpoints reduces attack surface—consolidating delete functionality into
POST /api/skills/deletewith middleware is more secure - The
requireLoopbackOriginmiddleware now guards both/api/skills/deleteand/api/skills/quarantineendpoints - Origin header validation using a Set of loopback hosts (
127.0.0.1,localhost,::1,[::1]) provides browser-based attack protection - Local services are not inherently safe—browser-based attacks can target localhost endpoints
How Orbis AppSec Detected This
- Source: HTTP DELETE request to
/api/skills/:idwith user-controlledidparameter - Sink:
deleteIds([req.params.id])inserver/index.js:131which removes files from the filesystem - Missing control: No authentication middleware, no origin validation, no authorization check before the destructive operation
- CWE: CWE-306 (Missing Authentication for Critical Function)
- Fix: Removed the unprotected DELETE endpoint and added
requireLoopbackOriginmiddleware to validate that requests originate from localhost before allowing delete and quarantine operations
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This vulnerability demonstrates a common oversight in local service development: assuming that localhost access is inherently trusted. The skill-cabinet server's DELETE endpoint was a single HTTP request away from allowing any local process—or malicious browser code—to delete user data.
The fix properly implements defense in depth by removing unnecessary attack surface (the standalone DELETE endpoint) and adding origin validation to remaining destructive endpoints. When building local services, always consider that browsers can make requests to localhost, and implement appropriate access controls accordingly.