fix(middleware): prevent cross-org site binding in target create/update

Extend verifySiteAccess to check that when req.userOrgId is already set
by a prior middleware (e.g. verifyResourceAccess/verifyTargetAccess), the
site from req.body.siteId belongs to the same organization. This prevents
the cross-organization tunnel boundary bypass where an attacker with
resource access in one org binds that resource's target to a site in
another org.

Add verifySiteAccess to both target route stacks:
- PUT /resource/:resourceId/target (after verifyResourceAccess)
- POST /target/:targetId (after verifyTargetAccess)

The org-match check runs before req.userOrg is overwritten, so the
resource's organization context is preserved for comparison.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
This commit is contained in:
Marc Schäfer
2026-05-29 22:44:16 +00:00
parent 71756812b6
commit 51629247a5
2 changed files with 14 additions and 5 deletions

View File

@@ -71,6 +71,15 @@ export async function verifySiteAccess(
);
}
if (req.userOrgId && site.orgId !== req.userOrgId) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this site"
)
);
}
if (!req.userOrg) {
// Get user's role ID in the organization
const userOrgRole = await db
@@ -128,10 +137,7 @@ export async function verifySiteAccess(
.where(
and(
eq(roleSites.siteId, site.siteId),
inArray(
roleSites.roleId,
req.userOrgRoleIds!
)
inArray(roleSites.roleId, req.userOrgRoleIds!)
)
)
.limit(1)

View File

@@ -561,6 +561,7 @@ authenticated.delete(
authenticated.put(
"/resource/:resourceId/target",
verifyResourceAccess,
verifySiteAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createTarget),
logActionAudit(ActionsEnum.createTarget),
@@ -612,6 +613,7 @@ authenticated.get(
authenticated.post(
"/target/:targetId",
verifyTargetAccess,
verifySiteAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateTarget),
logActionAudit(ActionsEnum.updateTarget),
@@ -1234,7 +1236,8 @@ export const authRouter = Router();
unauthenticated.use("/auth", authRouter);
authRouter.use(
rateLimit({
windowMs: config.getRawConfig().rate_limits.auth.window_minutes * 60 * 1000,
windowMs:
config.getRawConfig().rate_limits.auth.window_minutes * 60 * 1000,
max: config.getRawConfig().rate_limits.auth.max_requests,
keyGenerator: (req) =>
`authRouterGlobal:${ipKeyGenerator(req.ip || "")}:${req.path}`,