Repository navigation
Harden auth deactivation, remove unauthenticated OTP endpoints, stop routing table drain - #112
Open
memplethee-lab wants to merge 8 commits into
Open
memplethee-lab wants to merge 8 commits into
memplethee-lab wants to merge 8 commits into
Conversation
signIn() never checked user.isActive, so an admin deactivating a user via PATCH /admin/users/:id/toggle-status had no effect on password login - the account kept working. Every other auth path in this service (requestUserOtp, verifyUserOtp, AdminService.login, AdminService.verifyOtp) already checks it. The check is placed AFTER bcrypt.compare and reports the same generic 'Invalid credentials' message. Checking status first would return a distinct message for a disabled account and turn this endpoint into a user-enumeration oracle. Documented in the method comment so the order is not refactored away. Closes LintError#14 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POST /auth/otp/process and POST /auth/otp/cleanup were unauthenticated and unguarded despite being labelled admin endpoints - AuthController carries no @UseGuards at all. /otp/process forced OTP generation for every pending request on demand, so an anonymous caller could trigger generation and harvest the codes from the service log. /otp/cleanup deleted OTP rows older than 30 days, so it was an anonymous data-deletion endpoint. Both operations already run on cron - processPendingOtpRequests every 30 seconds and cleanupExpiredOtps daily - which was the stated intent in the original comments, so the HTTP triggers added no capability. Closes LintError#15 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The OTP cron wrote every generated code to stdout:
console.log(\[OTP Cron] Generated OTP \ for user ...\)
Combined with the removed unauthenticated trigger, that turned log access into
an OTP-disclosure channel. The code is now never logged - only the request id
and user email - with a comment so it is not reintroduced.
Replaces the console.log/console.error calls in this service with the NestJS
Logger, which routes through the app's log pipeline and honours log levels.
Closes LintError#15
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fallback branch assigned viable_chains = self.available_chains - an alias, not a copy - and then used heapq.heappop, which removes elements in place. Every request whose amount exceeded all chain liquidity permanently deleted up to limit chains from the long-lived routing table. Verified the drain: 100 fallback calls took the table from 8 entries to 0. Routing degraded with request history until process restart, which is what made payments start failing for reasons that depended on earlier traffic. - Copy the fallback list with list(...) instead of aliasing it. - Use heapq.nsmallest, which does not mutate its input, replacing the manual heapify/heappop loop. - Clamp limit so a non-positive or oversized value cannot misbehave. The list-comprehension branch was already safe and is left alone. Closes LintError#16 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
analyze_routing returned a null optimal_route when no route was viable.
PaymentsService.initiateTransaction dereferenced that unconditionally, so an
unroutable transaction surfaced as an unhandled TypeError ("Cannot read
properties of null") - a 500 triggered by ordinary traffic once the routing
table had been drained.
Return 503 with a real message when no route is viable, and re-raise
HTTPException so the 503 is not swallowed into a generic 500 by the enclosing
except block.
Closes LintError#16
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
initiateTransaction read routingResult.optimal_route.chain unconditionally. When the routing engine had no viable route the field was null, so the call threw "TypeError: Cannot read properties of null" - an unhandled 500 that surfaced as an internal error rather than a dependency failure. Narrow the route once after the fraud check and throw ServiceUnavailableException when it is absent, so the caller gets a 503 and no transaction record is written or broadcast. Makes RoutingResult.optimal_route explicitly nullable in AiService so the type reflects reality instead of forcing a non-null assertion at the call site. Closes LintError#16 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds regression coverage for LintError#14, LintError#15 and LintError#16. Each test was written to fail against the unfixed code. LintError#14 - signIn: - unknown email, wrong password, deactivated user, active user - a deactivated user with a correct password is rejected - no token is issued and lastLogin is not recorded for a rejected login - the message for unknown / wrong-password / deactivated is identical, so the endpoint cannot be used as a user-enumeration oracle - the password is compared before the account status is checked LintError#15 - removed routes: - POST /auth/otp/process and /auth/otp/cleanup return 404 - neither reaches the service methods - no HTTP handler remains bound to the OTP generation or cleanup jobs - the public /auth/otp/request and /auth/otp/verify routes still resolve LintError#16 - routing: - the table survives 100 consecutive fallback calls with its contents intact - repeated calls return identical results - a freshly constructed service and a heavily used one agree, so results do not depend on call history - results are sorted, bounded by limit, and limit is clamped - _to_usd documents the USD unit for liquidity filtering - an unavailable route raises ServiceUnavailableException, is not a TypeError, broadcasts nothing, and writes no transaction record Closes LintError#14 Closes LintError#15 Closes LintError#16 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The AI Engine job only ran a syntax check, so no Python test in ai/tests was ever executed. Add a unittest discovery step so the routing regression tests added for LintError#16 actually run. `-t .` sets ai/ as the top-level directory, which is what lets the tests use the same `from services... import ...` style as main.py. Closes LintError#16 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Summary
Fixes three assigned defects across the NestJS API and the Python AI engine: an
authentication bypass on deactivated accounts, two unauthenticated OTP admin
endpoints, and a routing service that destructively consumed its own chain list
and degraded over time.
#14 — deactivated accounts could still sign in
AuthService.signInnever checkeduser.isActive, soPATCH /admin/users/:id/toggle-statushad no effect on email/password login.Every other auth path in the codebase already checks it (
requestUserOtp,verifyUserOtp,AdminService.login,AdminService.verifyOtp), which is whatmade this easy to miss in review.
The check is placed after
bcrypt.compareand reuses the generic'Invalid credentials'message. Checking status first would return a distinctmessage for a disabled account and turn
POST /auth/logininto auser-enumeration oracle. The ordering is documented in the method comment so it
is not refactored away.
#15 — unauthenticated OTP admin endpoints
POST /auth/otp/processandPOST /auth/otp/cleanupwere labelled "Adminendpoint" but
AuthControllercarries no@UseGuards, so both were anonymous./otp/processforced OTP generation for every pending request on demand whilethe service logged each generated code to stdout, and
/otp/cleanupdeleted OTProws older than 30 days.
Both operations already run on cron —
processPendingOtpRequestsevery 30seconds and
cleanupExpiredOtpsdaily — which was the stated intent in theoriginal comments, so the HTTP triggers added no capability. Both removed.
Separately, the OTP cron no longer logs the code. It logs the request id and
user email only, so log aggregation cannot become an OTP-disclosure channel. The
console.*calls in the service are replaced with the NestJSLogger.#16 — routing table was drained by ordinary traffic
find_best_routesassignedviable_chains = self.available_chains(an alias,not a copy) and then used
heapq.heappop, which removes elements in place. Eachrequest whose amount exceeded every chain's liquidity permanently deleted up to
limitchains from the long-lived routing table.I reproduced the drain: 100 fallback calls took the table from 8 entries to
0. After that
optimal_routebecameNone, andPaymentsService.initiateTransactiondereferenced it unconditionally — anunhandled
TypeError: Cannot read properties of null500 triggered by ordinarytraffic.
list(...)) instead of aliasing it.heapq.nsmallest, which does not mutate its input, replacing the manualheapify/heappoploop; clamplimitso a non-positive or oversized valuecannot misbehave.
ai/main.pyreturns 503 when no route is viable, so it never emits a nulloptimal_route.HTTPExceptionis re-raised so the 503 is not swallowed intoa generic 500.
PaymentsServicenarrows the route and throwsServiceUnavailableException,so no record is written and nothing is broadcast.
RoutingResult.optimal_routeis now explicitly nullable rather than forcing a lie at the type level.
Verification
Per your instruction I did not run
npm install, CI, or the test suites.What I did run:
algorithms with equivalent semantics: old drained 8 → 0 over 100 calls, new
stayed at 8 with identical, deterministic results.
.tsfiles withtsc --noResolve(parse only, no module resolution, sincenode_modulesis absent).ai/tests/test_payment_routing.pyhas never been executed. It is writtenagainst
unittestand the import style matchesmain.py, but it is unverifiedand is the first thing I would run manually.
CI needed one change to exercise the new tests: the AI Engine job only ran
python -m compileall, so no Python test was ever executed. Added aunittest discoverstep (-t .makesai/top-level so the tests can use thesame
from services... import ...style asmain.py).Notes for review
_to_usdis a documented passthrough, not real conversion. Issue Stop payment_routing.find_best_routes from destructively consuming the shared chain list #16 askedfor a unit-consistent comparison;
RouteOption.liquidityis USD-denominatedwhile
amountis an arbitrary token amount. Without a price feed wired in, theonly honest option is to document the assumption and leave filtering behaviour
unchanged, which is what I did. Wiring
price_analysisinto this is the realfollow-up and I did not want to invent a rate.
for a
warningskey soPaymentsServicecan decide whether to proceed withunder-liquidity chains. That is a response-contract change affecting the AI
engine, its client type, and the payments path. Bundling it here would have
made the security fixes hard to review. The 503 covers the crash; the warning
channel is worth its own issue.
tokenVersionfollow-up from Check isActive in AuthService.signIn so deactivated users cannot log in with a password #14 is not implemented (a new column, amigration, and a
JwtStrategycheck). Tokens issued before deactivationremain valid until they expire. Left as a TODO referencing the issue.
requestUserOtpleaks account existence via'User not found'. That is a separate enumeration path and I did not changeit here.
POST /auth/otp/processand/auth/otp/cleanupnow return404. Anything relying on them needs the cron path instead.
Closes #14
Closes #15
Closes #16