Skip to content

fix(backend): stop leaking internal error details in 500 responses - #603

Open
iamwhitehat wants to merge 1 commit into
Protocol-Guild:mainfrom
iamwhitehat:fix/495-no-error-detail-leak
Open

fix(backend): stop leaking internal error details in 500 responses#603
iamwhitehat wants to merge 1 commit into
Protocol-Guild:mainfrom
iamwhitehat:fix/495-no-error-detail-leak

Conversation

@iamwhitehat

Copy link
Copy Markdown

Follow-up to #495.

The leak

29 catch blocks across 9 controllers responded with error.message directly:

res.status(500).json({ error: error.message });

When the failing call is a database query, that message carries table names,
column names and raw SQL fragments. A request that triggers a Postgres error
hands the attacker a free schema map for aiming injection attempts. Library
errors can also leak file paths and stack details.

Reproduction: send POST /api/taxes/rules with a payload that makes the insert
fail. The response body contains insert into "tax_rules" ... relation "tax_rules" does not exist, including the exact column list from the query.

The fix

New shared helper backend/src/utils/internalError.ts (sendInternalError):

  • Production returns one fixed generic message per call site. No error text
    ever reaches the client.
  • The full error is logged server-side with request id, method and path, so
    an incident report maps straight to the log entry holding the real cause.
  • Development keeps detailed messages in the response for fast debugging,
    where the leak is not exploitable. This mirrors what app.ts's global handler
    already did correctly; controllers now match it.

Migrated catch blocks in: paymentController (7), taxController (6),
multiSigController (5), cashFlowForecastController (4), assetController (3),
pdfCertificateController (3), bulkImportController (1), contractController (1),
contractRegistryController (1).

Not changed: authController's TwoFactorError path. Those messages are
deliberate client-facing errors with stable codes; only unexpected errors fall
through to the generic 500.

Also not changed here: contractUpgradeController and freezeController use
error.message for routing decisions or upstream Stellar/Horizon detail on
502, which needs its own review against acceptance criterion 4 of the issue;
happy to follow up separately rather than widen this diff.

Testing

New regression suite internalErrorLeak.test.ts: drives a real
Postgres-shaped schema error through the tax routes via supertest and asserts
no fragment (tax_rules, relation, insert into) appears in the response
body while the same error does land in server logs.

  • Unpatched code: fails, response echoes insert into "tax_rules" ...
  • With this change: passes

The existing contractRegistry.test.ts "returns 500 when service throws" test
previously asserted the leak as expected behavior (message: 'Registry load failed'); tightened into a leak regression guard instead.

Typecheck clean apart from a pre-existing parse failure in
tenantConfigService.ts (fails identically on clean main). Controllers suite:
61 passing before and after; the 21 pre-existing failures on clean main
(employee/search/contractEvent suites) are untouched by this diff.

Controllers were returning error.message directly to clients. When the
failing call is a database query, that message carries table names,
column names and SQL fragments, giving attackers exactly the schema
information needed to aim injection attempts.

Adds a shared sendInternalError helper:
- production responses return one fixed generic message per call site
- the full error is logged server-side with request id and path
- development keeps detailed messages for fast debugging

Migrated 29 catch blocks across 9 controllers (payment, tax, multiSig,
asset, cashFlowForecast, pdfCertificate, bulkImport, contract,
contractRegistry). TwoFactorError handling in authController is
unchanged: those messages are intentional client-facing errors.

Tests: new internalErrorLeak.test.ts drives a real Postgres-shaped
schema error through the tax routes and asserts no fragment of it
reaches the response body while it does land in server logs; fails on
unpatched code, passes with this change. contractRegistry 500 test
tightened into a leak regression guard.
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