One engine for Salesforce and the rest of your stack
A Salesforce org rarely stands alone. Around it there is usually a small fleet of services nobody thinks of as "the Salesforce project": a Node service that receives webhooks and writes them into the org, a Python job that copies records into a warehouse, a Go API behind the customer portal, a Java service that holds the integration credentials. They are written by different teams, deployed by different pipelines and reviewed, if at all, by different tools.
An attacker does not see that boundary. If the first hop of the path is a route in the Node service and the last hop is a field in the org, a review that looks at only one side sees half a path and calls it two unrelated medium findings.
One attack path across Salesforce and your services
Take a pattern that is common in companies that put a customer portal in front of Salesforce data.
A sync job copies invoices from the org into the portal's own database every few minutes. It authenticates as an integration user, and because "it needs to see everything to sync everything", that user holds View All on the invoice object.
The portal's API serves those invoices to signed-in customers:
// src/routes/invoices.ts
router.get('/api/invoices/:id', requireLogin, async (req, res) => {
// Signed in, yes. But whose invoice is this?
const invoice = await db.invoice.findUnique({ where: { id: req.params.id } });
res.json(invoice);
});
Read separately, each half looks ordinary:
- In the org: an integration user with View All on one object. Broad, but it is a sync job, and the user cannot log in interactively.
- In the service: a route that requires a login and looks a record up by id. It has authentication, and the query is parameterised, so there is no injection.
Read together, they are one sentence: any customer who can sign in to the portal can read any other customer's invoice, bank details included, by changing one number in the URL. The integration user's breadth is what filled the table with every customer's data; the missing ownership check is what hands it out.
The service half has a name. OWASP's API Security Top 10 puts broken object level authorization first in its 2023 edition, and defines object level authorization as a check, usually in code, that a user can only reach the objects they should. No configuration review will find a missing line of code, and no code review that stops at the org's edge will find it either.
What the services around the org need
The analysis that makes Salesforce findings useful, knowing every way in, what each entry point runs as and what it reaches, is exactly what the services need too. In concrete terms:
Every route, with its handler and its guards
You cannot check authorization on routes you do not know about. Vulkro Core maps the entry points the frameworks declare: Express, Fastify, Koa, Hono, NestJS, Next.js route handlers and server actions, tRPC and GraphQL resolvers in JavaScript and TypeScript; Spring MVC and WebFlux, JAX-RS, Quarkus, Micronaut and servlets in Java; Django, Django REST Framework, FastAPI, Flask, Starlette and aiohttp in Python; gin, echo, fiber, chi, gorilla/mux, net/http, gRPC and connect-go in Go. Each route names the function that handles it, its full path with every router and mount prefix applied, and the guards in front of it.
vulkro discover .
prints that inventory on its own, with each route marked protected, unprotected or unknown.
An authorization check, not just an authentication check
"Requires a login" is not the same as "is allowed to see this record". Vulkro Core decides, per route, whether it is authenticated, whether the record it loads is scoped to the caller, and whether a role is required. The findings that come out of that are the ones a login check alone hides: lookups by id with no ownership check, role changes with no role check, Next.js server actions without authorization, route groups with no guard, and request bodies written straight to the database.
Following the data across files
A request value rarely reaches the database in the handler that received it. Vulkro Core follows it inside a function, between functions in a file, and across files by following which functions call which, up to four calls deep, in JavaScript, TypeScript, Python and Go. Java is followed within one file, and PHP, C and C++ within one function: the Core page lists those limits rather than leaving them to be discovered.
Proof on every finding, and severity from reach
Every finding is marked proven when the path from the entry point to the risky call is complete, unproven when a hop could not be established, and not checked when that code was not analysed. Severity then comes from who can reach the code (anyone, any signed-up user, an internal user, an administrator), what is at stake (other users' data, account takeover, code execution, internal network reach), whether the path is proven, and whether a guard stands in the way. Each finding says why it got its severity.
For the authorization and injection classes, vulkro prove goes one step further: it writes a test file you can run in your own toolchain to confirm the finding is real. Vulkro only writes the file; it never runs it.
Offline, because the services hold the keys
The integration services are where the Salesforce credentials live. They are exactly the code you least want to upload to someone else's analysis service. Vulkro Core runs on your machine and in your pipeline, online, offline or air-gapped. The detection engine calls no model, and vulnerability data for dependency checks arrives as a checksummed bundle, so an air-gapped scanner does not go stale.
The same engine, on both sides
Vulkro Core is the engine Vulkro for Salesforce is built on. That has practical consequences beyond the marketing line:
- The same vocabulary. Proven, unproven and not checked mean the same thing in an Apex finding and in a TypeScript finding. So does a severity that comes from who can reach the code.
- The same editor. In a Salesforce DX workspace, the VS Code extension runs both language servers side by side: Apex and metadata go to Vulkro for Salesforce, the rest of the repository to Vulkro Core, and no finding is published twice.
- The same agent tools. Both scanners run as MCP servers, so an AI coding agent asks for the finding and the lines instead of reading the whole codebase to look for them.
To be precise about where the join happens today: the org and the services are scanned by two tools that produce two reports. The boundary between them, the integration users and the named credentials, is where you read the two together, and both reports describe it in the same terms. In the example above, the Salesforce side reports View All on the invoice object (SF-OBJ-PERM-001) and who holds it, and the service side reports the lookup by id with no ownership check, each with the evidence behind it.
Start with the service closest to the org
If you already scan your Salesforce code and org, the highest-value next step is the service that talks to the org most: the one with the integration user's credentials.
curl -fsSL https://dist.vulkro.com/install.sh | bash
vulkro scan .
Nothing is uploaded. The Vulkro Core page covers what it finds and how it is measured, including the published misses on the benchmark page. To install, start here.
Sources
- OWASP API Security Project, API1:2023 Broken Object Level Authorization: its first-place ranking in the 2023 edition and the definition of object level authorization.
