security: fix log injection, CORS wildcard default, WS auth docs, and… - #150
Open
allhandghost wants to merge 1 commit into
Open
Conversation
… WS connection limit This commit resolves four security issues identified in the vortex-backend audit. Each fix is described below with the affected files and approach taken. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Issue stellar-vortex-protocol#47 — Audit path parameters for log injection (log-injection via originalUrl) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ File: src/common/logging.interceptor.ts Problem: LoggingInterceptor logged request.originalUrl verbatim. A crafted request with URL-encoded newlines (%0a / %0d) or other ASCII control characters could inject fake log lines, corrupt structured log output, or confuse log-aggregation pipelines. Fix: Added a sanitizeForLog() helper that strips all C0/C1 control characters (U+0000–U+0008, U+000A–U+001F, U+007F–U+009F) except horizontal tab before the URL is interpolated into the log message. Horizontal tab is kept because it is harmless in most log formats. The sanitised URL is used only for logging — it is never written back into the response. All @Param() usages in IntentsController, SolversController, and SorobanController were audited: none reflect their raw value directly into logs or error messages in a way that bypasses the interceptor. Closes stellar-vortex-protocol#47 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Issue stellar-vortex-protocol#48 — CORS_ORIGIN must not default to wildcard in production ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Files: src/config/env.validation.ts | src/config/configuration.ts | .env.example Problem: CORS_ORIGIN defaulted to "*" unconditionally. A production deployment that forgot to set the variable would silently accept requests from any origin, enabling credentialed-adjacent cross-site requests against the API. Fix: - env.validation.ts: added a Joi.when() conditional on NODE_ENV. In production, CORS_ORIGIN is .required() and .invalid("*") — the app will refuse to start (validation error at boot) if the variable is missing or set to the wildcard. In development/test it still defaults to "*" for developer convenience. - configuration.ts: exposed wsMaxConnections in AppConfig (see stellar-vortex-protocol#50 below). - .env.example: added inline documentation explaining the production requirement and an example value (https://app.vortex.trade), plus the new WS_MAX_CONNECTIONS variable. Closes stellar-vortex-protocol#48 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Issue stellar-vortex-protocol#49 — Document WebSocket gateway authentication decision ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ File: src/intents/intents.gateway.ts Problem: IntentsGateway accepted every connection with no auth check and no comment explaining why. The absence of any decision record left ambiguity about whether the lack of auth was intentional. Decision (intentionally public read-only): The WS intent feed is a PUBLIC, READ-ONLY stream by design — analogous to Stellar Horizon event streams or dYdX order-book feeds. Transparency is a protocol property; any wallet or UI should be able to watch the feed without credentials. Solver bots that need to write (accept, fill) must use the REST API, which can be separately authenticated. The gateway never accepts inbound messages that trigger privileged actions, so there is nothing to protect here. A detailed JSDoc comment block was added above the class to make this decision explicit and to guide future contributors who might otherwise add ad-hoc auth later. If a private authenticated stream is ever needed it should live at a separate path (e.g. /ws/solver) with a NestJS WsGuard. Closes stellar-vortex-protocol#49 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Issue stellar-vortex-protocol#50 — Max-connections guard and subscriber count metric for WS gateway ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Files: src/intents/intents.gateway.ts | src/config/env.validation.ts src/config/configuration.ts | .env.example Problem: The subscribers Set grew unbounded. A connection-flood attack could exhaust file descriptors and heap memory, denying service to legitimate users. There was also no way to observe the current connection count for alerting. Fix: - WS_MAX_CONNECTIONS env var (default 1000, 0 = unlimited) validated by Joi in env.validation.ts and surfaced as wsMaxConnections in AppConfig. - IntentsGateway now injects ConfigService<AppConfig, true> and reads wsMaxConnections at construction time. - handleConnection() checks subscribers.size before adding the new client. If the limit is already reached the socket is closed immediately with RFC 6455 close code 1013 ("Try Again Later") and a human-readable reason, and a WARN log is emitted. The client is never added to the subscriber set, so the cap is never exceeded. - Added get subscriberCount(): number as a public getter so health checks, metrics endpoints, and future observability tooling (issue stellar-vortex-protocol#65) can read the live count without coupling to the gateway's internals. - Added NestJS Logger for structured debug-level connection lifecycle events (connected / error-drop / disconnected) with the live subscriber count. Closes stellar-vortex-protocol#50
|
@allhandghost Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
… WS connection limit
This commit resolves four security issues identified in the vortex-backend audit. Each fix is described below with the affected files and approach taken.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Issue #47 — Audit path parameters for log injection (log-injection via originalUrl) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ File: src/common/logging.interceptor.ts
Problem:
LoggingInterceptor logged request.originalUrl verbatim. A crafted request
with URL-encoded newlines (%0a / %0d) or other ASCII control characters
could inject fake log lines, corrupt structured log output, or confuse
log-aggregation pipelines.
Fix:
Added a sanitizeForLog() helper that strips all C0/C1 control characters
(U+0000–U+0008, U+000A–U+001F, U+007F–U+009F) except horizontal tab
before the URL is interpolated into the log message. Horizontal tab is
kept because it is harmless in most log formats. The sanitised URL is
used only for logging — it is never written back into the response.
All @Param() usages in IntentsController, SolversController, and
SorobanController were audited: none reflect their raw value directly into
logs or error messages in a way that bypasses the interceptor.
Closes #47
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Issue #48 — CORS_ORIGIN must not default to wildcard in production ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Files: src/config/env.validation.ts | src/config/configuration.ts | .env.example
Problem:
CORS_ORIGIN defaulted to "*" unconditionally. A production deployment
that forgot to set the variable would silently accept requests from any
origin, enabling credentialed-adjacent cross-site requests against the API.
Fix:
Closes #48
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Issue #49 — Document WebSocket gateway authentication decision ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ File: src/intents/intents.gateway.ts
Problem:
IntentsGateway accepted every connection with no auth check and no comment
explaining why. The absence of any decision record left ambiguity about
whether the lack of auth was intentional.
Decision (intentionally public read-only):
The WS intent feed is a PUBLIC, READ-ONLY stream by design — analogous to
Stellar Horizon event streams or dYdX order-book feeds. Transparency is a
protocol property; any wallet or UI should be able to watch the feed without
credentials.
Solver bots that need to write (accept, fill) must use the REST API, which
can be separately authenticated. The gateway never accepts inbound messages
that trigger privileged actions, so there is nothing to protect here.
A detailed JSDoc comment block was added above the class to make this
decision explicit and to guide future contributors who might otherwise add
ad-hoc auth later. If a private authenticated stream is ever needed it
should live at a separate path (e.g. /ws/solver) with a NestJS WsGuard.
Closes #49
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Issue #50 — Max-connections guard and subscriber count metric for WS gateway ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Files: src/intents/intents.gateway.ts | src/config/env.validation.ts
src/config/configuration.ts | .env.example
Problem:
The subscribers Set grew unbounded. A connection-flood attack could exhaust
file descriptors and heap memory, denying service to legitimate users.
There was also no way to observe the current connection count for alerting.
Fix:
Closes #50