🔒

API Audit Findings

Diagnostic.ly · Password required
Wrong password. Try again.

API Audit Findings

Engineering review with 23 findings across security, reliability, and API design. Includes severity ratings, affected files, and remediation roadmap.

Diagnostic.ly API Audit Findings

Engineering Review & Remediation Recommendations

Date: February 16, 2026

Repo: wbtw-repositories/diagnostic.ly-nodejs

Prepared for: Engineering Team


Summary

A comprehensive review of the diagnostic.ly-nodejs codebase revealed 23 findings across 6 categories. These range from security concerns that should be addressed immediately to API design inconsistencies that create friction for integrating partners.

Severity Breakdown

SeverityCountDescription
CRITICAL3Security vulnerabilities that need immediate attention
HIGH6Issues that affect reliability, data integrity, or partner experience
MEDIUM8Design inconsistencies that increase integration friction
LOW6Quality-of-life improvements and technical debt

CRITICAL Findings

C-1: Unauthenticated Endpoints Exposing Write Operations

Severity: CRITICAL

Files: src/routes/addressVerify.route.ts, src/routes/spot.route.ts, src/routes/channelPartners.route.ts

Several endpoints that perform write operations have no authentication middleware:

EndpointIssue
PUT /api/address-verify/ordersUpdates order address – no auth AND no schema validation
POST /api/spot/create/orderCreates orders via SpotDx – no auth
PUT /api/spot/update/orderUpdates order status – no auth
PUT /api/spot/get/order-statusRetrieves/syncs order data – no auth
PUT /api/channel-partner/user/:idUpdates channel partner user – auth is commented out
POST /api/hash/decodeDecodes hashed IDs – no auth

Risk: Any external actor who discovers these endpoints can modify order addresses, create orders, update order statuses, or decode internal IDs without credentials.

Recommendation:

  • Immediately add validateAccount or validateAccountV2 middleware to all write endpoints
  • Uncomment and verify the auth middleware on the channel partner user update route
  • Add schema validation to PUT /api/address-verify/orders
  • Audit all routes for missing auth – consider a default-deny pattern where routes without explicit auth middleware fail to register

C-2: PII Encryption Key Concern

Severity: CRITICAL

Files: src/model/Users.ts, src/constants/user.ts

User PII fields (email, name, last_name, phone_number, address, address2, city, state, country, postal, country_code, password, username) are AES-encrypted in MySQL using 'provisioning' as the encryption key.

Risk: If this key is hardcoded in the codebase or stored in a plain environment variable, anyone with database access or source code access can decrypt all patient PII. This is a HIPAA compliance concern for a healthcare platform.

Recommendation:

  • Verify the encryption key is stored in a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) – not in the codebase or .env file
  • Consider migrating to AWS KMS-managed encryption or application-level encryption with key rotation support
  • Audit who has access to the encryption key
  • Document the encryption approach for SOC 2 / HIPAA compliance

C-3: No Rate Limiting

Severity: CRITICAL

Files: src/app.ts (Express configuration)

The API has a 50MB max payload size and no rate limiting on any endpoint. There is no evidence of rate limiting middleware (e.g., express-rate-limit) in the codebase.

Risk: The API is susceptible to:

  • Denial-of-service via large payload floods
  • Credential brute-force attacks (auth returns the same 400 status for all failures)
  • Resource exhaustion from bulk order creation
  • Cost escalation from unbounded third-party API calls (ShipHero, Crelio, PostGrid, etc.)

Recommendation:

  • Implement rate limiting per API key (e.g., 100 requests/minute for standard, higher for trusted partners)
  • Add specific limits for expensive operations (POST /api/order – 10/minute, GET /api/test-results – 30/minute)
  • Return 429 Too Many Requests with Retry-After header
  • Consider API gateway-level rate limiting (AWS API Gateway, Kong, etc.)

HIGH Findings

H-1: Auth Errors Return 400 Instead of 401/403

Severity: HIGH

Files: src/middlewares/auth.ts

Every authentication and authorization failure returns 400 ClientError:

  • Missing API key → 400
  • Invalid API key → 400
  • Inactive account → 400
  • Method not permitted → 400

Impact: Integrators cannot programmatically distinguish between “your request body is malformed” and “your credentials are wrong.” This makes automated error handling and monitoring difficult for partner systems.

Recommendation:

  • Missing/invalid API key → 401 Unauthorized
  • Inactive account/entity → 403 Forbidden
  • Method not permitted → 403 Forbidden
  • Keep 400 for actual request validation errors

H-2: Outbound Webhooks Only Run Every 12 Hours

Severity: HIGH

Files: Cron configuration, src/controller/webhook.controller.ts

Order status update webhooks are dispatched by a cron job that runs every 12 hours. For a healthcare diagnostics platform where timely status updates drive clinical workflows, this is a significant delay.

Example scenario: A specimen arrives at the lab at 8:00 AM. The partner lab’s system won’t be notified until the next cron run, potentially 12 hours later. This delays specimen processing and result turnaround.

Recommendation:

  • Increase cron frequency to every 5-15 minutes for order status webhooks
  • Better: implement event-driven webhooks that fire immediately when an order status changes (via a Bull queue job triggered on status update)
  • Add a POST /api/order/:id/webhook/retry endpoint for partners to request an immediate re-send

H-3: No Webhook Retry with Exponential Backoff

Severity: HIGH

Files: src/controller/webhook.controller.ts, src/controller/testResults.controller.ts

Failed outbound webhooks are logged in webhook_logs with webhook_failed = 1, but there is no automatic retry mechanism. If a partner’s endpoint is temporarily down, they miss the update permanently.

The Crelio inbound webhook has a basic reprocessing mechanism (20-minute window), but outbound partner webhooks do not.

Recommendation:

  • Implement retry logic with exponential backoff (retry at 1m, 5m, 30m, 2h, 12h)
  • Use the existing Bull/Redis queue infrastructure for retry scheduling
  • Add a max_retries configuration (e.g., 5 attempts) per support entity
  • Provide a webhook delivery log endpoint so partners can see delivery status
  • Send an alert if retries are exhausted

H-4: Test Results Marked as “Sent” Before Partner Confirms Processing

Severity: HIGH

Files: src/controller/testResults.controller.ts

The test results outbound webhook sets enable_webhook_notification = 0 (marking results as sent) immediately after receiving an HTTP 200 from the partner. However:

  • The partner may return 200 at the network level but fail to process internally
  • There is no confirmation/acknowledgment mechanism
  • Results won’t be re-sent once marked

Recommendation:

  • Consider an acknowledgment protocol: send results, partner returns 200, then partner calls a confirmation endpoint within X minutes
  • Alternatively: keep results in a “pending confirmation” state and re-send if not confirmed within a window
  • At minimum: provide an endpoint for partners to request re-delivery of results for a specific order

H-5: No Pagination on User List Endpoint

Severity: HIGH

Files: src/controller/users.controller.ts

GET /api/user returns all users for an account with no pagination. For accounts with thousands of patients, this will:

  • Cause request timeouts
  • Consume excessive memory on the server
  • Return massive JSON payloads to the client

Recommendation:

  • Add page and limit query parameters (consistent with test results endpoint)
  • Default to 50 results per page, max 100
  • Include pagination object in the response (total, page, limit, totalPages)
  • Keep the current behavior as a fallback for backward compatibility but deprecate it

H-6: V1 Order Creation Controller Still in Codebase

Severity: HIGH

Files: src/controller/orders.controller.ts (134KB)

The V1 order creation controller is a 134KB monolithic file that:

  • Uses non-transactional database queries (DB pool instead of DBTx connection)
  • Has all logic inline (no helper extraction)
  • Has simpler error handling (no rollback on failure)

While the route currently points to V2 (createOrderV2), the V1 code is still importable and could be accidentally referenced.

Recommendation:

  • Verify no other code paths invoke V1 order creation
  • If confirmed unused, remove the V1 controller or clearly mark it as deprecated
  • If V1 is still used by some code path, prioritize migrating those callers to V2
  • The 134KB file should be refactored regardless – it’s unmaintainable at that size

MEDIUM Findings

M-1: Inconsistent Response Shapes Across Endpoints

Severity: MEDIUM

The API returns different response structures depending on the endpoint:

EndpointResponse Shape
Orders{ "statusCode": 200, "message": "...", "data": {...} }
Users{ "message": "...", "data": {...} } (no statusCode)
Test Results{ "status": "success", "statusCode": 200, "message": "...", "data": [...] }
Webhooks{ "success": true, "message": "..." }
Address Verify{ "message": "...", "verifiedAddress": {...} }

Impact: Partners must write different response parsing logic for each endpoint. This increases integration time and error-handling complexity.

Recommendation: Standardize all responses to a single envelope:

{
  "status": "success" | "error",
  "statusCode": 200,
  "message": "...",
  "data": { ... },
  "pagination": { ... }  // when applicable
}

M-2: Gender Value Inconsistency Between Users and Orders

Severity: MEDIUM

Files: src/validations/user.ts, src/validations/order.ts

ContextAccepted Values
User creation (user_type: 1)"Male", "Female"
Order ship_to.gender"Male", "Female", "Others" (case-insensitive)

A patient registered with the Users API cannot have a gender of "Others", but the same patient as an order recipient can. This creates data inconsistency.

Recommendation: Align gender values across all endpoints. For a healthcare platform, consider adopting HL7 FHIR administrative gender codes: male, female, other, unknown.


M-3: Phone Validation Uses Two Different Patterns

Severity: MEDIUM

Files: src/validations/order.ts, src/constants/common.ts

ContextPatternExample Match
Orders (ship_to.phone)^\+\d{10,15}$+12125551234 (11-16 digits total)
Users/Fax^\+\d{1,3}\d{8,10}$+12125551234 (10-14 digits total)

A phone number valid under one pattern may be rejected by the other. This causes confusion when partners create a user and then place an order for the same person with the same phone number.

Recommendation: Standardize on a single phone validation pattern across all endpoints. Consider using a phone validation library (e.g., libphonenumber) for proper international number validation.


M-4: order_Id Response Field Uses Inconsistent Casing

Severity: MEDIUM

Files: src/helpers/createOrder.ts

The order creation response returns order_Id (capital I) while every other field and endpoint uses snake_case (order_id, account_id, user_id).

Impact: Integrators who auto-map snake_case fields will miss this, causing silent bugs.

Recommendation: Change to order_id for consistency. This is a breaking change, so it should be communicated to existing partners.


M-5: declined Status Uses Lowercase While All Others Use Title Case

Severity: MEDIUM

Files: src/constants/status.ts

StatusCasing
ProcessingTitle Case
ApprovedTitle Case
SnoozedTitle Case
declinedlowercase
In TransitTitle Case

Impact: Integrators doing case-sensitive string comparisons against status values will miss declined orders if they expect Declined.

Recommendation: Change to Declined for consistency. Update any status comparison logic to be case-insensitive during the transition.


M-6: DELETE Endpoints Use Request Body Instead of URL Parameters

Severity: MEDIUM

Files: src/routes/users.route.ts, src/routes/groups.route.ts

DELETE /api/user and DELETE /api/group expect the resource identifier in the request body:

DELETE /api/user
{ "account_id": {...}, "user": { "gdt_id": 123 } }

Per HTTP specification (RFC 7231), a DELETE request body has no defined semantics. Many HTTP clients, proxies, CDNs, and API gateways strip or ignore bodies on DELETE requests.

Recommendation: Move to URL-parameter-based deletion:

  • DELETE /api/user/:id?account_id=LAB-001
  • DELETE /api/group/:id?account_id=LAB-001

M-7: user_type Enum Skips Values (No 4, 6, 9)

Severity: MEDIUM

Files: src/validations/user.ts

Valid user_type values: 1, 2, 3, 5, 7, 8, 10 – with gaps at 4, 6, and 9.

Impact: Integrators reading the docs will wonder what types 4, 6, and 9 are. If these were deprecated roles, their IDs should be documented as reserved. If they never existed, the numbering is confusing.

Recommendation: Document the gaps explicitly (e.g., “Values 4, 6, 9 are reserved”) or provide named constants that partners should use instead of raw numbers.


M-8: No Webhook Payload Signature Verification

Severity: MEDIUM

Files: src/controller/webhook.controller.ts

Outbound webhooks include a Bearer token in the Authorization header, but there is no HMAC signature on the request body. Partners cannot verify that the payload was actually sent by Diagnostic.ly and wasn’t tampered with in transit.

Recommendation:

  • Add an X-Signature-256 header containing HMAC-SHA256(webhook_secret, request_body)
  • Document how partners should verify the signature
  • This is standard practice (GitHub, Stripe, Shopify all do this)

LOW Findings

L-1: gdt_client_id Required on Test Results Endpoint

Severity: LOW

Files: src/validations/testResults.ts

The GET /api/test-results endpoint requires gdt_client_id as a query parameter. This is an internal system identifier that external integrators shouldn’t need to know or provide – it should be resolved from their API key or account.

Recommendation: Resolve gdt_client_id from the authenticated account context instead of requiring it as an explicit parameter.


L-2: Joi Validation Returns Only First Error

Severity: LOW

Files: src/middlewares/validate.ts

When a request has multiple validation errors, only the first error message is returned. Partners must fix and re-submit repeatedly to discover all issues.

Recommendation: Return all validation errors at once:

{
  "type": "ClientError",
  "statusCode": 400,
  "message": "Validation failed",
  "errors": [
    { "field": "ship_to.email", "message": "The field email is required" },
    { "field": "bundles[0].sku", "message": "Either sku or external_bundle_id is required" }
  ]
}

L-3: No API Versioning Strategy

Severity: LOW

The API has no version prefix (/v1/, /v2/). The V2 order creation is a different controller behind the same route. Breaking changes would affect all partners simultaneously.

Recommendation:

  • Introduce /v1/ prefix for current endpoints
  • When breaking changes are needed, create /v2/ endpoints while keeping /v1/ alive with a deprecation timeline
  • Include an API-Version response header

L-4: starting_order_status Commented Out in Joi But Validated at Runtime

Severity: LOW

Files: src/validations/order.ts, src/helpers/createOrder.ts

The starting_order_status field is commented out of the Joi validation schema but is still validated at runtime in the order creation helper. Invalid values pass Joi but fail later with less clear error messages.

Recommendation: Either uncomment and properly validate in Joi, or remove the runtime validation if the field is not intended for external use.


L-5: Non-Standard api-key Header Name

Severity: LOW

Files: src/middlewares/auth.ts

The API uses a custom api-key header instead of the standard Authorization: Bearer pattern.

Impact: Minor friction for integrators who expect standard auth headers. Some API tools and gateways have built-in support for Authorization but not custom headers.

Recommendation: Long-term, migrate to Authorization: Bearer or Authorization: ApiKey . Support both headers during transition.


L-6: Test Results Date Range Capped at 90 Days with Warning Instead of Error

Severity: LOW

Files: src/controller/getTestResult.controller.ts

When a date range exceeds 90 days, the API returns a 200 response with a warning field and empty data instead of a proper error response. Partners may not notice the warning and think there are simply no results.

Recommendation: Return a 400 error with a clear message: "Date range cannot exceed 90 days. Please narrow your search." This is unambiguous and forces correct usage.


Immediate (This Sprint)

  • C-1: Add auth to unauthenticated write endpoints
  • C-2: Audit PII encryption key storage
  • C-3: Implement basic rate limiting

Next Sprint

  • H-1: Fix auth error status codes (401/403)
  • H-2: Increase webhook cron frequency
  • H-3: Add webhook retry with backoff
  • H-4: Add test results re-delivery mechanism

Backlog

  • H-5: Add pagination to user list
  • H-6: Remove/deprecate V1 order controller
  • M-1 through M-8: Address consistency issues
  • L-1 through L-6: Quality-of-life improvements

This audit was performed by static analysis of the diagnostic.ly-nodejs source code. Runtime testing and penetration testing are recommended to identify additional issues.