Review shared middleware twice: once as an implementation, and again at each materially different call site. Identify which deployed services and versions use it; what trusted identity or policy it expects; what it actually checks; and what each downstream service must still enforce. Trace one allowed path, one denied path and one bypass or alternate route. A safe middleware function does not prove every caller invokes it or passes the right object. A risky function does not prove every route is exposed.
The method changes when the middleware runs at a gateway, inside each service, or only in a build-time adapter. Capture that placement before reaching a verdict. OWASP's microservices guidance distinguishes ingress authorization from service-level checks that need resource and business context. A source trace is evidence about code behavior; deployment bindings and practical exploitability may need separate confirmation.
Establish what “shared” actually means
In a monorepo, packages/auth might be imported by five services but packaged into three deployed binaries at different revisions. Another service may copy an old helper. A gateway may use its own implementation. The first output of the review should be a consumer map, not a vulnerability count.
For each consumer, record the package commit or resolved version, the route or handler that calls it, the order in which middleware runs, any bypass configuration, and the protected operation that follows. Include internal workers, message consumers, retries and direct RPC routes if they call the same data layer. Review the deployment manifest or lockfile rather than assuming source proximity means identical runtime behavior. If a service resolves a published package instead of the workspace package, confirm the actual deployed artifact with its owner.
Separate middleware responsibilities:
| Responsibility | Typical middleware decision | What may still be required downstream |
|---|---|---|
| Authentication | Is this session or service token valid? | Permission for the selected action and object. |
| Coarse route authorization | May this role call this route? | Ownership and tenant match for the concrete resource. |
| Tenant context | Which tenant is selected for this request? | Whether the principal can act for it and the stored object belongs to it. |
| Input checks | Are fields well formed and within bounds? | Business invariants and output authorization. |
| Audit context | Which subject and request ID should be recorded? | Accurate event at the actual data mutation or delivery. |
A middleware layer can deliberately make a complete authorization decision if it receives authoritative resource facts and the service verifies the bound decision. It is a design choice to inspect, not an assumption. OWASP's authorization patterns guidance discusses policy enforcement placement and why an edge check may lack object-specific context.
Illustrative case: an accountContext package
Illustrative case. Fictional Clearline Cloud has packages/account-context, services/projects, services/reports, and workers/report-export. The package validates a signed session and sets request.account_id. Projects and reports both import it. The export worker consumes queue jobs and does not run HTTP middleware. The example describes a review method; no repository or production system was tested.
The initial alert says “report endpoint lacks tenant authorization.” To investigate, the reviewer writes down the relevant execution sequence. The following is abstract pseudocode with explicit sequence, not a claim about any framework's middleware registration semantics:
projects detail:
ctx = accountContextV3(verifiedSession)
project = store.findProject(ctx.account, request.projectId)
return project
reports legacy download:
ctx = accountContextV3(verifiedSession)
ctx.account = request.query.account // overwrites trusted context
report = store.findReport(request.reportId)
response.send(report.bytes) // sensitive effect occurs here
require(report.account == ctx.account) // too late, even if it throws
export worker:
job = loadJob(message.jobId)
permit = jobPolicyV1(job) // separately published package
require(permit.allowed)
render(job.reportId)
Projects uses the account from validated context in its object lookup. In the fictional accessor, that pair is enforced before a row is returned. Reports has two problems: the caller replaces the trusted account with user input, and the comparison occurs after delivery. Moving the comparison earlier alone would leave the attacker-controlled account comparison. Removing the overwrite alone would leave the late check. The corrected path must preserve trusted account context and compare the stored report owner before bytes or a download capability leave the service.
The middleware source can be entirely correct while this consumer is wrong. Conversely, changing accountContextV3 will not necessarily repair the worker: its deployment record resolves job-policy version 1 from a published artifact, not the workspace helper being edited. Version 1 checks only that the job exists and is ready. That is insufficient to settle report ownership, but it is not proof of a worker flaw if a trusted admission path already binds an immutable job to an authorized report. Trace job creation, all writers and the worker accessor before choosing where a missing boundary must be repaired.
Record the change radius, not a package-wide verdict
| Consumer | Concrete boundary result | Change and evidence required |
|---|---|---|
| Projects detail | Trusted account reaches an accessor that enforces account plus project ID | Preserve this contract; examine the accessor implementation and relevant error path. No demonstrated change needed for this example. |
| Reports legacy download | Trusted context overwritten; guard after delivery | Make context immutable to handler input; load report with authorized account or check stored owner before output; trace foreign report and matching report paths. |
| Export worker | Published V1 permit alone does not establish report ownership; upstream binding unexamined | Inspect admission, job integrity and accessor. If no mandatory authorized binding exists, add one at the appropriate boundary and verify the actual worker artifact. |
| Admin retry | Consumer discovered, deployed version unavailable | Keep open; do not inherit either the projects result or worker finding. |
The output is one source-supported reports defect, an unresolved worker-authorization question, and a deployment gap. It is not “the shared middleware is vulnerable everywhere.” The reports fix concerns how a correct helper's result is used. The worker needs evidence of its durable-job authorization contract, which may be enforced at trusted admission and preserved through processing rather than repeated as HTTP middleware.
A proposed V4 that adds authorizeReport(ctx, report) may offer a reusable implementation, but adding the function does not migrate callers. The handoff lists the legacy route to replace, the worker artifact to rebuild, and the admin consumer still to identify. A focused follow-up traces the old reports route and confirms that it no longer delivers before the decision; it also confirms that no alternative caller still overwrites context. These are expected checks for an authorized assessment, not executed results from this article.
The source examples establish the mechanism under their stated assumptions. Production exposure, actual deployment and independent confirmation remain necessary before describing a verified production issue. OWASP API1:2023 supplies the governing distinction: access to an endpoint does not itself grant access to the selected object.
Walk the implementation and the call sites
Read the middleware's entry and exit conditions. Which claims does it accept? Does it validate issuer, audience, expiry, signature and selected tenant as appropriate to the token profile? Where does it fetch current policy? What happens when policy service access fails? Are errors denied, retried or converted into an anonymous request? Do not treat a logged warning as enforcement. If the package calls a remote authorization service, note caching and revocation behavior. OWASP's microservices guidance discusses the distinction between token validation and service authorization, including freshness tradeoffs.
At each consumer, inspect the middleware order and route registration. A handler registered before the guard, a webhook exempted from it, or a websocket upgrade path may have a different boundary. Follow values after middleware: is account_id read from the validated context, overwritten by a path parameter, or copied into a queue message? Does the data accessor scope the lookup or only the list endpoint? Does the final response or download apply the same authorization rule?
Do a bounded negative-path review, even if the implementation appears clear:
- Wrong account: Trace what the code would do if the authenticated user supplies a report ID owned by another account.
- Missing context: Trace whether an internal caller can invoke the handler or data accessor without the HTTP middleware.
- Stale authority: Determine how membership or role changes reach the decision point, and mark unknown deployment/cache behavior.
- Alternate producer: Find every route that can create a report-export job, including support tools and retries.
- Output: Confirm the download's recipient and object binding, rather than stopping at job creation.
This is a source review plan, not an instruction to attack production. If controlled runtime validation is authorized, use a test tenant and non-sensitive fixtures, and record exactly which environment and version were exercised. A static analysis tool's configuration also matters: GitHub's CodeQL workflow options distinguish directories selected for analysis from paths that merely trigger a workflow. A missing alert in a consumer outside the analysis path proves little.
Make the migration record name the consumers
If Clearline introduces V4, retain a consumer migration table rather than closing the reports issue when the package release appears. The projects service may already satisfy its object rule and need only compatibility review. The legacy reports route needs the actual pre-delivery authorization correction. The worker needs its separate job-admission question resolved. Admin retry remains blocked on version mapping. These are different acceptance conditions even when one shared release supports them.
For each changed consumer, record the resolved package, caller contract, artifact and relevant behavior evidence. A lockfile update establishes a dependency selection in source; the running artifact mapping establishes whether the service received it. A copied legacy helper or direct lower-level accessor can remain outside the new package's protection, so use the consumer map to locate such paths.
Do not force every service to adopt a new abstraction merely to make the table uniform. A local scoped accessor may correctly enforce the rule, while another service needs the new helper. The goal is the same authorization outcome at the relevant effects, with maintainable ownership and explicit contracts. Consistency helps when it prevents omission; it is not evidence by itself.
The package owner should also describe which changes invalidate earlier caller evidence: new context fields, changed default behavior, altered error handling or a different policy source. That information lets application owners focus reassessment on the contract that changed, while preserving useful evidence for unaffected paths.
Decide the finding and the change radius separately
The engineer can conclude “source-supported candidate,” “counterevidence rejects this path,” or “inconclusive until route/policy evidence arrives.” Independent review should check the precise actor-to-effect path, not just whether the middleware line exists. Severity, practical difficulty and business priority remain separate from that verdict. For Clearline, the source trace identifies both the late reports check and the overwritten context. Deployment exposure and independent confirmation still bound the production claim.
If a defect is supported, define the change radius before suggesting a fix. A change to accountContext may affect every importer; a local report lookup fix may be narrower but leave the export worker exposed. The owning engineers should choose the smallest change that enforces the same object rule at every relevant route. Reassess the original path and affected consumers at the new pinned revision, and keep unrelated consumers outside the claimed verification scope. A passing build or disappearing alert does not by itself prove the authorization invariant.
Next step: Select one shared package and fill the consumer matrix for its highest-impact three callers. Use the boundary guide to locate the protected object and the large-monorepo scoping guide to make deferred consumers visible.