Skip to content

Conversation

@dev-dami
Copy link
Owner

@dev-dami dev-dami commented Jan 24, 2026

Summary

This PR addresses critical and important security issues identified during codebase review:

Critical Fixes

  • Authentication: Added bearer token authentication to HTTP API
  • Path Traversal: Fixed vulnerability allowing ../ in service names
  • Input Validation: Service names now validated for Docker compatibility

Important Improvements

  • Rate Limiting: Added configurable rate limiting (60 req/min default)
  • Non-root Containers: Both Bun and Node Dockerfiles now run as ignite user (UID 1001)
  • Audit Mode via HTTP: Exposed audit option in HTTP API for sandboxed execution
  • Health Check: /health endpoint now bypasses auth for load balancer compatibility
  • Memory Leak Fix: Rate limiter cleanup interval cleared on server stop

Changes

File Purpose
packages/http/src/server.ts Auth, rate limit, path traversal fix, audit mode
packages/http/src/types.ts Added audit option to request type
packages/core/src/service/load-service.ts Docker-compatible name validation
packages/runtime-bun/Dockerfile Non-root user
packages/runtime-node/Dockerfile Non-root user

Usage

import { createServer } from '@ignite/http';

const server = createServer({
  port: 3000,
  apiKey: process.env.IGNITE_API_KEY,  // Enable auth
  rateLimit: 30,                        // 30 req/min
  rateLimitWindow: 60000,               // 1 minute window
});

Testing

  • Build passes
  • TypeScript typecheck passes
  • Manual testing with Docker

Breaking Changes

None - all new features are opt-in.

Summary by CodeRabbit

  • New Features

    • Added API key authentication for securing server endpoints.
    • Added rate limiting with configurable request limits and time windows.
    • Added optional audit flag to service execution requests for tracking and compliance.
  • Security & Infrastructure

    • Configured containers to run as non-root user for enhanced security posture.
    • Added service name validation enforcing Docker-compatible naming conventions.

✏️ Tip: You can customize this high-level summary in your review settings.

…ening

- Add bearer token authentication to HTTP API (optional but recommended)
- Add rate limiting with configurable limits (60 req/min default)
- Fix path traversal vulnerability in serviceName parameter
- Validate service names for Docker compatibility (1-63 chars, lowercase alphanumeric with hyphens)
- Expose audit mode via HTTP API for sandboxed execution
- Add non-root user (ignite:1001) to both Bun and Node Dockerfiles
- Fix /health endpoint to bypass authentication for load balancer checks
- Fix memory leak: clear rate limiter cleanup interval on server stop

BREAKING CHANGE: None - all new features are opt-in
@coderabbitai
Copy link

coderabbitai bot commented Jan 24, 2026

📝 Walkthrough

Walkthrough

The pull request introduces Docker-compatible naming validation for services, adds API key authentication and rate limiting to the HTTP server, includes optional audit support for service execution requests, and hardens container runtimes by running them as non-root users.

Changes

Cohort / File(s) Summary
Service name validation enforcement
packages/core/src/service/load-service.ts, packages/http/src/server.ts
Added Docker-compatible regex validation for service names in config loading; applied validation at request-handling stage for execute and preflight endpoints to prevent path traversal and invalid names.
API authentication & rate limiting
packages/http/src/server.ts
Introduced optional API key authentication, in-memory rate limiter with configurable window and request limits, and automatic cleanup interval; extended ServerOptions with apiKey, rateLimit, and rateLimitWindow fields.
Audit request support
packages/http/src/types.ts, packages/http/src/server.ts
Added optional audit?: boolean field to ServiceExecutionRequest interface; wired audit flag through POST /services/:serviceName/execute handler.
Container runtime security
packages/runtime-bun/Dockerfile, packages/runtime-node/Dockerfile
Created non-root system user (ignite, UID 1001), updated file ownership for application files and entrypoint scripts, and switched container execution to run as the non-root user.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 With Docker names validated clean,
Rate limits guard our server's scene,
Audit trails now track the way,
Non-root users save the day—
Security hops into the light! 🔒✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main security-focused changes: authentication, rate limiting, and container hardening, matching all critical improvements in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

@dev-dami dev-dami merged commit 39b1f18 into master Jan 24, 2026
4 of 5 checks passed
Copy link

@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: 3

🤖 Fix all issues with AI agents
In `@packages/http/src/server.ts`:
- Around line 26-27: The inline comment above SERVICE_NAME_REGEX is incorrect:
it says "2-63 chars" while the regex alternation (SERVICE_NAME_REGEX) allows a
single-character name and the error message later states "1-63 chars"; update
the comment to match the regex and error text (e.g., "1-63 chars") so the
comment accurately reflects SERVICE_NAME_REGEX and the validation behavior.
- Around line 137-140: Replace the direct string comparison of token !== apiKey
with a constant-time comparison: convert both token and apiKey to Buffers
(ensuring lengths match) and use crypto.timingSafeEqual to compare them; handle
the case where token is missing or lengths differ by failing the check and
keeping the same response behavior (set.status = 401; throw new Error(...)).
Update the check around the token variable in the request handling block (the
code that currently uses token and apiKey) to perform this buffered, timing-safe
comparison and preserve the existing error message and status on mismatch.
- Around line 123-131: The current extraction of client IP in the .derive(({
request, set }) => { ... }) middleware reads
request.headers.get('x-forwarded-for') raw and falls back to 'unknown', which is
spoofable and improperly parsed; update this to (1) support a configuration flag
(e.g., trustProxy / trustedProxies) that determines whether to use proxied
headers or direct socket address, (2) when trusting proxies, use a proven
library like proxy-addr (or equivalent) with an explicit trustedProxies
whitelist to resolve the client IP instead of reading the header directly, (3)
if you cannot use proxy-addr, at minimum parse x-forwarded-for by taking the
first non-empty, trimmed token before any comma and validate it, (4) when not
trusting proxies, use request.socket.remoteAddress (or framework equivalent) as
the client IP, and (5) remove the 'unknown' shared-bucket fallback—require a
resolved IP and handle missing/invalid values by denying or using a distinct
per-request fallback; pass the normalized IP to rateLimiter.check(clientIp) and
keep setting set.status and Retry-After as before.
🧹 Nitpick comments (2)
packages/http/src/server.ts (2)

118-122: Consider adding a comment explaining why /health is defined before derive.

The placement of /health before the derive middleware intentionally bypasses authentication and rate limiting for load balancer compatibility. A brief comment would make this intent explicit and prevent accidental reordering.

Suggested comment
+    // Health check endpoint defined before derive() to bypass auth and rate limiting
+    // for load balancer compatibility
     .get('/health', (): HealthResponse => ({

246-260: Consider using static import for consistency.

readdir is dynamically imported here, but join and resolve from node:path are statically imported at the top. Since this is server-side code, a static import would be more consistent.

Suggested change

Add to imports at top:

-import { join, resolve } from 'node:path';
+import { join, resolve } from 'node:path';
+import { readdir } from 'node:fs/promises';

Then simplify the handler:

     .get('/services', async ({ set }): Promise<{ services: string[] } | ErrorResponse> => {
       try {
-        const { readdir } = await import('node:fs/promises');
         const entries = await readdir(resolvedServicesPath, { withFileTypes: true });

Comment on lines +26 to +27
// Service name validation: lowercase alphanumeric with hyphens, 2-63 chars (Docker compatible)
const SERVICE_NAME_REGEX = /^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$|^[a-z0-9]$/;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor: Comment states "2-63 chars" but regex allows single-character names.

The comment says "2-63 chars" but the regex alternation |^[a-z0-9]$ allows single-character names. The error message on line 44 correctly states "1-63 chars".

Suggested fix
-// Service name validation: lowercase alphanumeric with hyphens, 2-63 chars (Docker compatible)
+// Service name validation: lowercase alphanumeric with hyphens, 1-63 chars (Docker compatible)
📝 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
// Service name validation: lowercase alphanumeric with hyphens, 2-63 chars (Docker compatible)
const SERVICE_NAME_REGEX = /^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$|^[a-z0-9]$/;
// Service name validation: lowercase alphanumeric with hyphens, 1-63 chars (Docker compatible)
const SERVICE_NAME_REGEX = /^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$|^[a-z0-9]$/;
🤖 Prompt for AI Agents
In `@packages/http/src/server.ts` around lines 26 - 27, The inline comment above
SERVICE_NAME_REGEX is incorrect: it says "2-63 chars" while the regex
alternation (SERVICE_NAME_REGEX) allows a single-character name and the error
message later states "1-63 chars"; update the comment to match the regex and
error text (e.g., "1-63 chars") so the comment accurately reflects
SERVICE_NAME_REGEX and the validation behavior.

Comment on lines +123 to +131
.derive(({ request, set }) => {
const clientIp = request.headers.get('x-forwarded-for') || 'unknown';
const rateLimitResult = rateLimiter.check(clientIp);

if (!rateLimitResult.allowed) {
set.status = 429;
set.headers['Retry-After'] = String(rateLimitResult.retryAfter);
throw new Error(`Rate limit exceeded. Retry after ${rateLimitResult.retryAfter} seconds`);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

safely extract client IP x-forwarded-for header Node.js best practices

💡 Result:

Best practices (Node.js / Express) for safely extracting the real client IP

  1. Treat X-Forwarded-For as untrusted input unless you control the proxy chain. Clients can send/spoof X-Forwarded-* headers; only trust them when your app is definitely behind known reverse proxies/load balancers you control. The same general warning applies to the standardized Forwarded header too. [4]

  2. In Express, prefer req.ip / req.ips + correctly configured trust proxy (don’t manually split X-Forwarded-For). Express will select the client IP by walking the chain and finding the first untrusted address (right-to-left) based on your trust proxy configuration. Misconfiguring trust proxy can let clients influence what IP you see. [1]

  3. Configure trust proxy narrowly (CIDRs / known proxy IPs / exact hop count), not just true, whenever possible.

    • app.set('trust proxy', true) makes Express treat the left-most X-Forwarded-For value as the client IP (easy to spoof if requests can reach your app without going through your proxy). [1]
    • Prefer explicit IPs/CIDRs of your proxy tiers, or a fixed hop count only if the path length is guaranteed. Express documents pitfalls if multiple path lengths exist. [1]
  4. If you’re not using Express, use a battle-tested parser (proxy-addr) with a trust policy. Express uses proxy-addr internally; it returns the closest untrusted address according to your trust function/list. [2]

  5. Prefer single-purpose “real IP” headers from your edge provider when available (and only accept them from that provider).

    • Example (Cloudflare): CF-Connecting-IP / True-Client-IP contain a single IP and are recommended over X-Forwarded-For for origin apps/logs, but still must be trusted only when the request actually came from Cloudflare. [3]

Express example (recommended pattern)

import express from "express";

const app = express();

// Trust only your proxy/load balancer networks (examples; use YOUR real CIDRs/IPs).
app.set("trust proxy", [
  "10.0.0.0/8",
  "172.16.0.0/12",
  "192.168.0.0/16",
]);

app.get("/", (req, res) => {
  // Express picks the correct client IP based on trust proxy settings
  res.json({ ip: req.ip, chain: req.ips });
});

(How Express determines req.ip/req.ips and why correct trust proxy matters is described here. [1])


Plain Node / any framework example with proxy-addr

import proxyaddr from "proxy-addr";

const trust = proxyaddr.compile(["10.0.0.0/8", "172.16.0.0/12"]); // your proxies

function getClientIp(req) {
  return proxyaddr(req, trust); // closest untrusted address
}

(proxy-addr API and “closest untrusted address” behavior. [2])


Sources

[1] Express “Behind Proxies” (trust proxy, req.ip, right-to-left evaluation, hop-count caveats) (expressjs.com)
[2] proxy-addr documentation (closest untrusted address; compile trust) (npmjs.com)
[3] Cloudflare headers guidance (prefer CF-Connecting-IP / True-Client-IP; X-Forwarded-For behavior) (developers.cloudflare.com)
[4] RFC 7239 security considerations (forwarded headers can’t be relied on; whitelist trusted proxies) (pike.lysator.liu.se)

Citations:


🌐 Web query:

x-forwarded-for spoofing security risks rate limiting

💡 Result:

Why X‑Forwarded‑For (XFF) spoofing matters (especially for rate limiting)

X-Forwarded-For is just an HTTP request header. Unless you control and trust the proxy that sets it, the client can send any value they want, and many servers/frameworks will mistakenly treat it as the “real client IP” (often by taking the first IP in the list). This enables straightforward bypasses of IP-based controls like rate limiting and allow/deny lists. [2]

RFC 7239 (the standardized Forwarded header) explicitly warns that forwarding headers cannot be relied on because they can be modified by any node in the path (including the client), and even “whitelisting proxies” still doesn’t make the earlier IP chain trustworthy. [1]

Common security risks

  • Rate-limit bypass / abuse amplification: attacker rotates fake XFF values to appear as many different clients, evading per-IP throttles. [2]
  • Access-control bypass: systems that allow/deny based on “internal/trusted IPs” from XFF can be tricked by sending private/internal IPs in XFF. [4]
  • Bad audit logs / forensics gaps: logs attribute actions to spoofed IPs, harming incident response and abuse detection. (Implied by the header’s lack of integrity.) [1]
  • Incorrect geo/risk decisions: geo-fencing, fraud scoring, bot detection, and anomaly detection keyed to client IP become unreliable. [1]

How to do rate limiting safely when proxies/CDNs are involved

  1. Never trust XFF from the open Internet. Only use it if the request arrived from a trusted reverse proxy/CDN IP range that you control/explicitly trust. [1]
  2. At the edge, overwrite/normalize forwarding headers. Your edge proxy/CDN should set the “client IP” signal itself and ignore/strip client-supplied values (otherwise spoofed values remain at the front of XFF). [2]
  3. Prefer a single-IP header your provider guarantees at the origin. Example: Cloudflare recommends using CF-Connecting-IP (single IP) rather than XFF (list that may include client-supplied values). [3]
  4. Parse forwarding chains defensively. If you must consume XFF/Forwarded, don’t blindly take the first IP; determine the client IP relative to your known trusted proxy chain (because only the portion added by trusted infrastructure is meaningful). [1][2]
  5. Don’t key rate limits only on IP. Use stronger identifiers when available (API key, user ID, session, device fingerprint), with IP as a secondary signal—this was a key mitigation after real-world XFF-based rate-limit bypasses. [2]

Rule of thumb: if an attacker can send it as a header, it’s input—treat it as untrusted unless your network boundary guarantees otherwise. [1][2]

Sources: RFC 7239 security considerations [1]; StackHawk write-up on XFF rate-limit bypass and fixes [2]; Cloudflare header guidance (CF-Connecting-IP vs XFF) [3]; Acunetix advisory on XFF-based security bypass [4].


🌐 Web query:

x-forwarded-for comma separated multiple IPs format

💡 Result:

X-Forwarded-For (XFF) is a comma-separated list of IP addresses:

  • Format / syntax:
    X-Forwarded-For: <client>, <proxy1>, <proxy2> (commas may have optional surrounding whitespace). [1]
  • Ordering meaning (when proxies behave correctly):
    Leftmost = original client, and each proxy that forwards the request appends its own address to the right; rightmost = most recent proxy. [1]
  • Multiple header lines:
    A request can include multiple X-Forwarded-For headers; they must be treated as one combined list (effectively concatenated, in order, with commas). [1]
  • Security note:
    Don’t trust the leftmost value for security decisions unless you’re only accepting traffic through trusted proxies and you validate/strip incoming XFF appropriately. [1]

Example:
X-Forwarded-For: 203.0.113.195, 2001:db8::1, 198.51.100.178 [1]

Related standard header (preferred when supported): Forwarded (RFC 7239). [2]

Sources: [1] MDN X-Forwarded-For header, [2] RFC 7239.


Rate limiting can be bypassed via spoofed x-forwarded-for header without a trusted proxy configuration.

The code extracts client IP directly from the x-forwarded-for header without validating that the request came through a trusted reverse proxy, allowing clients to send arbitrary header values to bypass rate limits. Additionally, x-forwarded-for is comma-separated (format: client, proxy1, proxy2), but the code uses the raw header value without parsing. The 'unknown' fallback also creates a shared rate limit bucket that could be exploited.

Address this by:

  1. Documenting that this service must run exclusively behind a trusted reverse proxy that overwrites untrusted x-forwarded-for values
  2. Using a library like proxy-addr (or Express's trust proxy configuration) with an explicit whitelist of trusted proxy IPs/CIDRs rather than manual header extraction
  3. If direct client connection without proxies must be supported, add a configuration option to switch IP extraction strategies (e.g., use req.socket.remoteAddress when not proxied)
🤖 Prompt for AI Agents
In `@packages/http/src/server.ts` around lines 123 - 131, The current extraction
of client IP in the .derive(({ request, set }) => { ... }) middleware reads
request.headers.get('x-forwarded-for') raw and falls back to 'unknown', which is
spoofable and improperly parsed; update this to (1) support a configuration flag
(e.g., trustProxy / trustedProxies) that determines whether to use proxied
headers or direct socket address, (2) when trusting proxies, use a proven
library like proxy-addr (or equivalent) with an explicit trustedProxies
whitelist to resolve the client IP instead of reading the header directly, (3)
if you cannot use proxy-addr, at minimum parse x-forwarded-for by taking the
first non-empty, trimmed token before any comma and validate it, (4) when not
trusting proxies, use request.socket.remoteAddress (or framework equivalent) as
the client IP, and (5) remove the 'unknown' shared-bucket fallback—require a
resolved IP and handle missing/invalid values by denying or using a distinct
per-request fallback; pass the normalized IP to rateLimiter.check(clientIp) and
keep setting set.status and Retry-After as before.

Comment on lines +137 to +140
if (!token || token !== apiKey) {
set.status = 401;
throw new Error('Unauthorized: Invalid or missing API key');
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Timing attack vulnerability in API key comparison.

Direct string comparison with !== leaks timing information that could allow attackers to guess the API key character by character. Use a constant-time comparison function.

Suggested fix using crypto.timingSafeEqual
+import { timingSafeEqual } from 'node:crypto';
+
+function safeCompare(a: string, b: string): boolean {
+  if (a.length !== b.length) {
+    // Compare against itself to maintain constant time even on length mismatch
+    const buf = Buffer.from(a);
+    timingSafeEqual(buf, buf);
+    return false;
+  }
+  return timingSafeEqual(Buffer.from(a), Buffer.from(b));
+}

// Then in the derive block:
-        if (!token || token !== apiKey) {
+        if (!token || !safeCompare(token, apiKey)) {
🤖 Prompt for AI Agents
In `@packages/http/src/server.ts` around lines 137 - 140, Replace the direct
string comparison of token !== apiKey with a constant-time comparison: convert
both token and apiKey to Buffers (ensuring lengths match) and use
crypto.timingSafeEqual to compare them; handle the case where token is missing
or lengths differ by failing the check and keeping the same response behavior
(set.status = 401; throw new Error(...)). Update the check around the token
variable in the request handling block (the code that currently uses token and
apiKey) to perform this buffered, timing-safe comparison and preserve the
existing error message and status on mismatch.

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.

2 participants