Skip to content

Known defects & findings register

🔴 A five-day bot flood in July 2026, and no rate limiting today

Section titled “🔴 A five-day bot flood in July 2026, and no rate limiting today”

Between 2026-07-09 and 07-13 the platform absorbed roughly 384 million WAF requests against a ~30,000/day baseline — about 2,500× normal. It made AWS WAF the single largest line on that month’s bill (~$616, against a June WAF of ~$22 measured from its own request volume). (No share-of-the-month figure is quoted: Cost Explorer holds nothing before 2026-08-12, so no July denominator exists.)

The platform itself was never affected. The load balancer’s :443 listener routes on host header, the flood matched no rule, and the listener’s default fixed-response 503 answered it without ever consulting a target group: ELB 503s went from 4,116 to 87,364,520 while requests actually reaching targets stayed flat (22,413 → 21,805) and target 5XX errors stayed at zero. This was a cost incident, not an availability incident — worth stating, because “384 million requests” reads like an outage.

And it was self-inflicted, not an attack. A UAT hostname migration at 2026-07-09T00:37:41Z removed the retired hosts from the production ALB listener rules. That listener’s default action is fixed-response 503, so every client still calling the old names received an ALB-generated 503 that never reached a target — and retried. Onset was under three minutes, and the traffic ramped over 45 minutes then plateaued, which is retry backoff rather than an attack arriving at its rate.

The operational lesson, and it generalises: retiring a hostname on an ALB whose default action is fixed-response 503 silently converts every stale client into billable, inspected traffic — and nothing in this estate watches for it.

It went undetected for 71 hours. The flood began 2026-07-09T00:40Z; the first human action against it was 2026-07-12T09:43:57Z, and WAF blocking only took hold at 10:03:18Z — ~81 hours after onset. The HTTPCode_ELB_503_Count metric went ~21,000× and triggered nothing.

Five things about it are still true today:

  • There is no alarm on HTTPCode_ELB_503_Count — the one signal that would have caught this in minutes rather than days. It costs nothing.
  • A retired hostname is not kept on a 301/410 rule through a cutover, so it falls to the listener default. That is the cause, and preventing it is free.
  • There is no rate-based rule on the Web ACL. RateBasedStatement count: zero. Worth ~$1/month as a blast-radius cap on any volumetric event, whatever its origin.
  • There is no AWS Budget and no billing alarm. Cost Anomaly Detection exists and would probably fire for an event this size, but not for a smaller recurring step.
  • Every mitigating change was made by an account that no longer has access (the outgoing vendor’s). Nobody currently on the account has operated this failure mode.

No request-level forensics exist and none can be reconstructed — WAF logging was enabled on 2026-07-22, ten days after the event.

🔴 A failed or stopped blue/green deploy orphans its green fleet, and nothing cleans it up

Section titled “🔴 A failed or stopped blue/green deploy orphans its green fleet, and nothing cleans it up”

Green-fleet teardown is wired only to success. All three blue/green deployment groups set terminateBlueInstancesOnDeploymentSuccess and provision green with COPY_AUTO_SCALING_GROUP. There is no equivalent on any other terminal state, so a deployment that ends before the traffic shift leaves a running Auto Scaling group behind with no owner.

Auto-rollback does not cover this, and CodeDeploy says so itself. The failed production deployment’s rollbackInfo.rollbackMessage reads:

“Automatic rollback isn’t required because traffic hasn’t begun rerouting to the replacement environment.”

Auto-rollback governs traffic, not fleet lifecycle: blue never stopped serving, so there is nothing to roll back. A stopped deployment does not even reach that check — it returns an empty rollbackInfo.

Terminal state Traffic shifted? Rollback fires? Green fleet
Succeeded yes n/a becomes the live fleet
Failed before the shift no no — “not required” orphaned
Stopped no no — never evaluated orphaned

Both branches have already fired. A stalled CRM deploy on 2026-07-20 leaked a fleet that billed for five weeks before anyone noticed; a failed backend deploy on 2026-08-24 leaked one that was force-deleted by hand 35 minutes later. Since the cutover, 56 production blue/green deployments reached a terminal state, exactly two were non-success — and both leaked. (A failure only leaks if it happens after the green fleet is created and before the traffic shift; failures that end earlier — no instances, missing IAM permissions — never create one. Both of these were in that window, and both leaked.)

An emergency control can outlive its emergency

Section titled “An emergency control can outlive its emergency”

Two Allow-* rules sit at the top of the Web ACL and let matching traffic skip the entire managed rule set. Neither is an arbitrary mis-ordering: both are carve-outs added under incident pressure — one created mid-flood so an uptime monitor would not be caught by newly-aggressive blocking — and neither has been revisited since.

This is worth generalising. The CRM outage of 2026-07-22 has been recorded as caused by “pre-cutover” WAF hardening. It was not: it was the mitigation for this flood, and a service was later moved behind that mitigation without anyone knowing it was one. Check for emergency controls before a cutover — they become invisible constraints on the next change.

Finding Where Impact
Paymob webhook HMAC verification is entirely commented out — the callback accepts any body and trusts obj.success with no signature check app/Services/PaymobService.php:146-152 (guard call), :153 (trust) Critical — forged payment-success webhooks are accepted
Even the dead HMAC code uses == not hash_equals (non-constant-time) app/Services/PaymobService.php:210 Timing side-channel (moot while disabled, wrong if re-enabled)
No replay / idempotency guard on payment callbacks — a replayed success re-marks the transaction, re-submits to the CRM, and re-fires notifications app/Services/PaymentService.php:36-73 High — duplicate CRM writes / notifications
Payment callback routes are public / unauthenticated routes/api.php:31-38 High — combined with the above
check-whitelisted-emails is an unauthenticated GET that leaks whitelist membership and the brokerage source_id app/Http/Controllers/Api/Broker/UserController.php:116-121, routes/api/broker.php:72 Medium — enumeration / info leak
OTP throttles are keyed by the phone field value, not IP — rotating the phone value bypasses the bucket shopper OTP routes (routes/api/shopper.php) Medium — OTP brute-force / spam
EOI details GET forwards the raw {eoiSourceId} to the CRM with no local existence check (not a route-model binding) routes/api/shopper.php (EOI details) Low/Medium — CRM probing
Thawani secret can be sourced from a DB column payment_api_secret_key; Paymob integration id written to the debug log app/Services/ThawaniService.php:25, PaymobService.php:100 Low — secret handling
Finding Where Impact
getSalesOfferPdf route points at a controller method that does not exist — dead route, 500 on hit routes/api/broker.php:132 and routes/api/sales-man.php:79 (Base\SalesOfferPdfController has no such method) Broken endpoint (superseded by the async generate/status/download trio)
Sales-man logout resolves to a non-existent class (Api\SalesMan\Auth\MicrosoftController) — 500 “Target class does not exist”; the real class is App\Http\Controllers\SalesMan\Auth\MicrosoftController@logout routes/api/sales-man.php:22 Broken logout (SSO login works via routes/web.php)
Broker GET documents returns 4 hardcoded rows with document: "#" app/Http/Controllers/Api/Broker/UserController.php:24-52 Mock in production surface
Shopper GET user/referrals (“my referrals”) returns hardcoded mock data behind a // TODO: call CRM app/Http/Controllers/.../ReferralController.php:12-51 Mock in production surface
UnitClient::createResale() reads config('services.crms.unit.create_resale'), a key not declared in the config block → resolves to null crms-middleware/app/Clients/Crms/UnitClient.php:61 vs config/services.php:177-186 Resale-create silently broken
PreDeliveryUnitSaleResource and the UnitAddonPackageType enum are defined but referenced nowhere in app code app/Http/Resources/PreDeliveryUnitSaleResource.php; app/Enums/UnitAddonPackageType.php Dead code
Taskeen’s Broker code branch is dead (Taskeen is sales-man-only) app/Actions/Taskeen/GetTaskeenList.php:25-27 Dead code
Finding Where Impact
Shopper auth is Sanctum, not Passport (as backend/CLAUDE.md claims); config/auth.php sets the api guard driver to passport but routes gate on auth:sanctum app/Models/Shopper.php:11,16; config/auth.php Documentation + config mismatch
backend/CLAUDE.md §9 references app/Services/Payment/*, a directory that does not exist (payment code is flat under app/Services/) backend/CLAUDE.md:§9 Stale doc — misdirects searches
Frontend declares a @orascom/api tsconfig path alias pointing at libs/api/src/index.ts that does not exist frontend/tsconfig.base.json Dangling alias
PDF service fails closed on auth (unset API_TOKEN → 401 for all requests) even though its README/CLAUDE claim “no token ⇒ open” backend-pdf-service/src/routes/sales-offer.ts:9-12 Doc contradicts code
middleware-package config file is git-ignored / absent, so its provider’s config() reads resolve to null — it would fail to boot as checked out; its provider also auto-registers routes + webhook ingress against its own CLAUDE rules middleware-package/ (.gitignore last line; CrmIntegrationServiceProvider.php:48) Library unusable as-is (it is also unused: 0 imports platform-wide)
crm-mock PROJECT-OVERVIEW.md documents a POST /api/oman/brokers/documents route that is not in routes/api.php crm-mock/ Stale doc
PDF_SERVICE_URL default is http://localhost:3001 in config but docs cite the Docker host http://pdf-service:3001 backend/config/services.php:69 Minor drift
Finding Where Impact
There is no separate Payload CMS — all content is served by the Laravel Voyager admin over the same DB (the handover note “Payload CMS” is a mislabel) grep across repos (0 payload.config, 0 dep) Corrects a false architectural assumption
Firebase is auth-only, not FCM push — Kreait Firebase Auth verifies shopper tokens; there is no CloudMessage/device-token/push-send code anywhere app/Services/FirebaseAuthService.php:16; shopper LoginController.php:25 “Push notifications” don’t exist server-side
OpenTelemetry is not wired (no open-telemetry/* / @opentelemetry/* deps) — Sentry tracing covers the need composer.json / package.json Corrects an assumed integration
Pusher is config-scaffold only, not wired — no pusher-php-server dep, no ShouldBroadcast, default driver null/log, no frontend pusher-js config/broadcasting.php Broadcasting is inert
Egypt is hardcoded in several write paths: broker register and EOI submit (Country::where('slug','egypt')), add-on payment (Egypt-only), and Taskeen (Egypt-only CRM URL) UserController.php:74; AddonPayment.php:19; Taskeen middleware Non-Egypt markets silently unsupported on these paths
Currency-Layer is wired with a deliberate CURRENY_LAYER typo “workaround”; the frontend NX_CURRENY_LAYER_* keys are vestigial (unused) config/currency.php; frontend env Confusing but functional
Finding Where Impact
Precedence bug: if (! $response->getStatusCode() == 200) — ! binds tighter than ==, so the guard is never true and the intended exception can’t fire app/Services/PaymobService.php:69, ThawaniService.php:52 Error path unreachable
Resell-assistance reads $requestData['service_slug'], a key the FormRequest never collects → PHP 8.2 warning + service_type = null on every unit_resale row app/Http/Controllers/.../CustomerRequestController.php:102 Data always null
A referral is classified referral locally off portal_page, but as is_referral in the CRM off the referral_* fields — the two conditions can disagree app/Http/Controllers/.../LeadController.php:48-51 vs ShopperSubmitNewLead.php:41 Inconsistent classification
The unit hold is placed at create-sale (DRAFT), not at the reserve click, and share-reservation-link creates no hold — there is no local reservation TTL (expiry is CRM-owned) app/Actions/Sale/CreateSaleCrm.php:35; ReserveSaleCrm.php:31 Holds behave unintuitively
create_unit_type and mark_pipeline_as_lost have no egypt entry in the per-country validator map — a valid egypt header still 422s on those actions app/Http/Requests/SalesMan/Country/CountryRequestValidator.php:44-61 Egypt agents blocked on 2 actions
finish-sale is a PATCH that force-sets status = DONE with no FormRequest / body validation on the state transition routes/api/shopper.php (finish-sale) Unvalidated state change
Infobip non-2xx errors are swallowed; SMS silently no-ops when ALLOW_SEND_SMS=false even though the OTP row is stored/verifiable app/Services/SmsService.php; app/Notifications/SmsNotification.php Silent delivery failures
OTP code space is only ~10k with two fixed leading zeros (e.g. 009999): str_pad((string) random_int(0, 9999), 6, '0', ...) app/Actions/Otp/SendOtp.php:16-18 Weak OTP entropy
The commission-analytics enum case CACHED is a misspelling of “cashed” (= paid) app/Enums/BrokerCommissionAnalyticsStatus.php Cosmetic but confusing
handlePaymobResponseCallback (serves all Paymob callbacks — sale / add-on / EOI) contains a hardcoded sleep(10) app/Services/PaymentService.php:102 Blocks the request 10s