Skip to content

security: fix log injection, CORS wildcard default, WS auth docs, and… - #150

Open
allhandghost wants to merge 1 commit into
stellar-vortex-protocol:mainfrom
allhandghost:security/fix-issues-47-48-49-50
Open

security: fix log injection, CORS wildcard default, WS auth docs, and…#150
allhandghost wants to merge 1 commit into
stellar-vortex-protocol:mainfrom
allhandghost:security/fix-issues-47-48-49-50

Conversation

@allhandghost

Copy link
Copy Markdown

… 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:

  • 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 Add per-connection rate limiting and a max-connections guard to the WS gateway #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 #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:

  • 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 Guard BigInt(dto.fillAmount) against malformed input #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 #50

… 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
@drips-wave

drips-wave Bot commented Jul 27, 2026

Copy link
Copy Markdown

@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! 🚀

Learn more about application limits

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

Labels

None yet

Projects

None yet

1 participant