fix(security): normalize request parameters and update dependencies

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
This commit is contained in:
Marc Schäfer
2026-05-15 18:35:58 +00:00
parent dd1f7ba544
commit 18d380ce30
37 changed files with 2656 additions and 3609 deletions

View File

@@ -1,5 +1,5 @@
#! /usr/bin/env node
import "./extendZod.ts";
import "./extendZod";
import { runSetupFunctions } from "./setup";
import { createApiServer } from "./apiServer";

View File

@@ -0,0 +1,11 @@
export function getFirstString(value: unknown): string | undefined {
if (typeof value === "string") {
return value;
}
if (Array.isArray(value) && typeof value[0] === "string") {
return value[0];
}
return undefined;
}

View File

@@ -4,6 +4,7 @@ import { resourceAccessToken, resources, apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyApiKeyAccessTokenAccess(
req: Request,
@@ -12,7 +13,7 @@ export async function verifyApiKeyAccessTokenAccess(
) {
try {
const apiKey = req.apiKey;
const accessTokenId = req.params.accessTokenId;
const accessTokenId = getFirstString(req.params.accessTokenId);
if (!apiKey) {
return next(
@@ -20,6 +21,12 @@ export async function verifyApiKeyAccessTokenAccess(
);
}
if (!accessTokenId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid access token ID")
);
}
const [accessToken] = await db
.select()
.from(resourceAccessToken)

View File

@@ -4,6 +4,7 @@ import { apiKeys, apiKeyOrg } from "@server/db";
import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyApiKeyApiKeyAccess(
req: Request,
@@ -14,8 +15,10 @@ export async function verifyApiKeyApiKeyAccess(
const { apiKey: callerApiKey } = req;
const apiKeyId =
req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId;
const orgId = req.params.orgId;
getFirstString(req.params.apiKeyId) ||
getFirstString(req.body.apiKeyId) ||
getFirstString(req.query.apiKeyId);
const orgId = getFirstString(req.params.orgId);
if (!callerApiKey) {
return next(

View File

@@ -3,6 +3,7 @@ import { db, domains, orgDomains, apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyApiKeyDomainAccess(
req: Request,
@@ -12,8 +13,10 @@ export async function verifyApiKeyDomainAccess(
try {
const apiKey = req.apiKey;
const domainId =
req.params.domainId || req.body.domainId || req.query.domainId;
const orgId = req.params.orgId;
getFirstString(req.params.domainId) ||
getFirstString(req.body.domainId) ||
getFirstString(req.query.domainId);
const orgId = getFirstString(req.params.orgId);
if (!apiKey) {
return next(
@@ -27,6 +30,12 @@ export async function verifyApiKeyDomainAccess(
);
}
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
if (apiKey.isRoot) {
// Root keys can access any domain in any org
return next();

View File

@@ -4,6 +4,7 @@ import { idp, idpOrg, apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyApiKeyIdpAccess(
req: Request,
@@ -12,8 +13,12 @@ export async function verifyApiKeyIdpAccess(
) {
try {
const apiKey = req.apiKey;
const idpId = req.params.idpId || req.body.idpId || req.query.idpId;
const orgId = req.params.orgId;
const idpIdRaw =
getFirstString(req.params.idpId) ||
getFirstString(req.body.idpId) ||
getFirstString(req.query.idpId);
const idpId = Number.parseInt(idpIdRaw ?? "", 10);
const orgId = getFirstString(req.params.orgId);
if (!apiKey) {
return next(
@@ -27,7 +32,7 @@ export async function verifyApiKeyIdpAccess(
);
}
if (!idpId) {
if (Number.isNaN(idpId)) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid IDP ID")
);

View File

@@ -4,6 +4,7 @@ import { apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyApiKeyOrgAccess(
req: Request,
@@ -12,7 +13,7 @@ export async function verifyApiKeyOrgAccess(
) {
try {
const apiKeyId = req.apiKey?.apiKeyId;
const orgId = req.params.orgId;
const orgId = getFirstString(req.params.orgId);
if (!apiKeyId) {
return next(
@@ -45,7 +46,7 @@ export async function verifyApiKeyOrgAccess(
}
if (!req.apiKeyOrg) {
next(
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this organization"

View File

@@ -4,6 +4,7 @@ import { siteResources, apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyApiKeySiteResourceAccess(
req: Request,
@@ -12,7 +13,8 @@ export async function verifyApiKeySiteResourceAccess(
) {
try {
const apiKey = req.apiKey;
const siteResourceId = parseInt(req.params.siteResourceId);
const siteResourceIdRaw = getFirstString(req.params.siteResourceId);
const siteResourceId = Number.parseInt(siteResourceIdRaw ?? "", 10);
if (!apiKey) {
return next(
@@ -20,7 +22,7 @@ export async function verifyApiKeySiteResourceAccess(
);
}
if (!siteResourceId) {
if (Number.isNaN(siteResourceId)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,

View File

@@ -4,6 +4,7 @@ import { resources, targets, apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyApiKeyTargetAccess(
req: Request,
@@ -12,7 +13,8 @@ export async function verifyApiKeyTargetAccess(
) {
try {
const apiKey = req.apiKey;
const targetId = parseInt(req.params.targetId);
const targetIdRaw = getFirstString(req.params.targetId);
const targetId = Number.parseInt(targetIdRaw ?? "", 10);
if (!apiKey) {
return next(
@@ -20,7 +22,7 @@ export async function verifyApiKeyTargetAccess(
);
}
if (isNaN(targetId)) {
if (Number.isNaN(targetId)) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid target ID")
);

View File

@@ -7,6 +7,7 @@ import HttpCode from "@server/types/HttpCode";
import { canUserAccessResource } from "@server/auth/canUserAccessResource";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyAccessTokenAccess(
req: Request,
@@ -14,7 +15,7 @@ export async function verifyAccessTokenAccess(
next: NextFunction
) {
const userId = req.user!.userId;
const accessTokenId = req.params.accessTokenId;
const accessTokenId = getFirstString(req.params.accessTokenId);
if (!userId) {
return next(
@@ -22,6 +23,12 @@ export async function verifyAccessTokenAccess(
);
}
if (!accessTokenId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid access token ID")
);
}
const [accessToken] = await db
.select()
.from(resourceAccessToken)
@@ -87,7 +94,7 @@ export async function verifyAccessTokenAccess(
}
if (!req.userOrg) {
next(
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this organization"

View File

@@ -6,6 +6,7 @@ import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyApiKeyAccess(
req: Request,
@@ -14,9 +15,24 @@ export async function verifyApiKeyAccess(
) {
try {
const userId = req.user!.userId;
const apiKeyId =
req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId;
const orgId = req.params.orgId;
const apiKeyIdFromParams = getFirstString(req.params?.apiKeyId);
const apiKeyIdFromBody = getFirstString(req.body?.apiKeyId);
if (
apiKeyIdFromParams &&
apiKeyIdFromBody &&
apiKeyIdFromParams !== apiKeyIdFromBody
) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"API key ID provided in both URL and body with different values"
)
);
}
const apiKeyId = apiKeyIdFromParams || apiKeyIdFromBody;
const orgId = getFirstString(req.params.orgId);
if (!userId) {
return next(
@@ -104,10 +120,7 @@ export async function verifyApiKeyAccess(
}
}
req.userOrgRoleIds = await getUserOrgRoleIds(
req.userOrg.userId,
orgId
);
req.userOrgRoleIds = await getUserOrgRoleIds(req.userOrg.userId, orgId);
return next();
} catch (error) {

View File

@@ -6,6 +6,7 @@ import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyDomainAccess(
req: Request,
@@ -14,9 +15,8 @@ export async function verifyDomainAccess(
) {
try {
const userId = req.user!.userId;
const domainId =
req.params.domainId;
const orgId = req.params.orgId;
const domainId = getFirstString(req.params.domainId);
const orgId = getFirstString(req.params.orgId);
if (!userId) {
return next(
@@ -62,10 +62,7 @@ export async function verifyDomainAccess(
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, orgId)
)
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))
)
.limit(1);
req.userOrg = userOrgRole[0];

View File

@@ -3,6 +3,7 @@ import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { usageService } from "@server/lib/billing/usageService";
import { build } from "@server/build";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyLimits(
req: Request,
@@ -13,7 +14,10 @@ export async function verifyLimits(
return next();
}
const orgId = req.userOrgId || req.apiKeyOrg?.orgId || req.params.orgId;
const orgId =
req.userOrgId ||
req.apiKeyOrg?.orgId ||
getFirstString(req.params.orgId);
if (!orgId) {
return next(); // its fine if we silently fail here because this is not critical to operation or security and its better user experience if we dont fail

View File

@@ -6,6 +6,7 @@ import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyOrgAccess(
req: Request,
@@ -13,7 +14,7 @@ export async function verifyOrgAccess(
next: NextFunction
) {
const userId = req.user!.userId;
const orgId = req.params.orgId;
const orgId = getFirstString(req.params.orgId);
if (!userId) {
return next(

View File

@@ -1,10 +1,16 @@
import { Request, Response, NextFunction } from "express";
import { db, userOrgs, siteProvisioningKeys, siteProvisioningKeyOrg } from "@server/db";
import {
db,
userOrgs,
siteProvisioningKeys,
siteProvisioningKeyOrg
} from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
import { getFirstString } from "@server/lib/requestParams";
export async function verifySiteProvisioningKeyAccess(
req: Request,
@@ -13,8 +19,10 @@ export async function verifySiteProvisioningKeyAccess(
) {
try {
const userId = req.user!.userId;
const siteProvisioningKeyId = req.params.siteProvisioningKeyId;
const orgId = req.params.orgId;
const siteProvisioningKeyId = getFirstString(
req.params.siteProvisioningKeyId
);
const orgId = getFirstString(req.params.orgId);
if (!userId) {
return next(
@@ -80,10 +88,7 @@ export async function verifySiteProvisioningKeyAccess(
.where(
and(
eq(userOrgs.userId, userId),
eq(
userOrgs.orgId,
row.siteProvisioningKeyOrg.orgId
)
eq(userOrgs.orgId, row.siteProvisioningKeyOrg.orgId)
)
)
.limit(1);

View File

@@ -7,6 +7,7 @@ import HttpCode from "@server/types/HttpCode";
import { canUserAccessResource } from "../auth/canUserAccessResource";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyTargetAccess(
req: Request,
@@ -14,7 +15,8 @@ export async function verifyTargetAccess(
next: NextFunction
) {
const userId = req.user!.userId;
const targetId = parseInt(req.params.targetId);
const targetIdRaw = getFirstString(req.params.targetId);
const targetId = Number.parseInt(targetIdRaw ?? "", 10);
if (!userId) {
return next(

View File

@@ -4,6 +4,7 @@ import { userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyUserIsOrgOwner(
req: Request,
@@ -11,7 +12,7 @@ export async function verifyUserIsOrgOwner(
next: NextFunction
) {
const userId = req.user!.userId;
const orgId = req.params.orgId;
const orgId = getFirstString(req.params.orgId);
if (!userId) {
return next(

View File

@@ -19,6 +19,7 @@ import { eq, and } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyCertificateAccess(
req: Request,
@@ -27,11 +28,43 @@ export async function verifyCertificateAccess(
) {
try {
// Assume user/org access is already verified
const orgId = req.params.orgId;
const certId =
req.params.certId || req.body?.certId || req.query?.certId;
let domainId =
req.params.domainId || req.body?.domainId || req.query?.domainId;
const orgId = getFirstString(req.params.orgId);
const certIdFromParams = getFirstString(req.params?.certId);
const certIdFromBody = getFirstString(req.body?.certId);
if (
certIdFromParams &&
certIdFromBody &&
certIdFromParams !== certIdFromBody
) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Certificate ID provided in both URL and body with different values"
)
);
}
const certId = certIdFromParams || certIdFromBody;
const domainIdFromParams = getFirstString(req.params?.domainId);
const domainIdFromBody = getFirstString(req.body?.domainId);
if (
domainIdFromParams &&
domainIdFromBody &&
domainIdFromParams !== domainIdFromBody
) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Domain ID provided in both URL and body with different values"
)
);
}
let domainId = domainIdFromParams || domainIdFromBody;
if (!orgId) {
return next(
@@ -65,7 +98,7 @@ export async function verifyCertificateAccess(
);
}
domainId = cert.domainId;
domainId = cert.domainId ?? undefined;
if (!domainId) {
return next(
createHttpError(

View File

@@ -17,6 +17,7 @@ import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyIdpAccess(
req: Request,
@@ -25,8 +26,12 @@ export async function verifyIdpAccess(
) {
try {
const userId = req.user!.userId;
const idpId = req.params.idpId || req.body.idpId || req.query.idpId;
const orgId = req.params.orgId;
const idpIdRaw =
getFirstString(req.params.idpId) ||
getFirstString(req.body?.idpId) ||
getFirstString(req.query?.idpId);
const idpId = Number.parseInt(idpIdRaw ?? "", 10);
const orgId = getFirstString(req.params.orgId);
if (!userId) {
return next(
@@ -40,7 +45,7 @@ export async function verifyIdpAccess(
);
}
if (!idpId) {
if (Number.isNaN(idpId)) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID")
);

View File

@@ -18,6 +18,7 @@ import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyRemoteExitNodeAccess(
req: Request,
@@ -25,11 +26,11 @@ export async function verifyRemoteExitNodeAccess(
next: NextFunction
) {
const userId = req.user!.userId; // Assuming you have user information in the request
const orgId = req.params.orgId;
const orgId = getFirstString(req.params.orgId);
const remoteExitNodeId =
req.params.remoteExitNodeId ||
req.body.remoteExitNodeId ||
req.query.remoteExitNodeId;
getFirstString(req.params.remoteExitNodeId) ||
getFirstString(req.body?.remoteExitNodeId) ||
getFirstString(req.query?.remoteExitNodeId);
if (!userId) {
return next(
@@ -37,6 +38,15 @@ export async function verifyRemoteExitNodeAccess(
);
}
if (!orgId || !remoteExitNodeId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid organization or remote exit node ID"
)
);
}
try {
const [remoteExitNode] = await db
.select()

View File

@@ -16,40 +16,44 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { response as sendResponse } from "@server/lib/response";
import { getFirstString } from "@server/lib/requestParams";
import privateConfig from "#private/lib/config";
import { GenerateNewLicenseResponse } from "@server/routers/generatedLicense/types";
export interface CreateNewLicenseResponse {
data: Data
success: boolean
error: boolean
message: string
status: number
data: Data;
success: boolean;
error: boolean;
message: string;
status: number;
}
export interface Data {
licenseKey: LicenseKey
licenseKey: LicenseKey;
}
export interface LicenseKey {
id: number
instanceName: any
instanceId: string
licenseKey: string
tier: string
type: string
quantity: number
quantity_2: number
isValid: boolean
updatedAt: string
createdAt: string
expiresAt: string
paidFor: boolean
orgId: string
metadata: string
id: number;
instanceName: any;
instanceId: string;
licenseKey: string;
tier: string;
type: string;
quantity: number;
quantity_2: number;
isValid: boolean;
updatedAt: string;
createdAt: string;
expiresAt: string;
paidFor: boolean;
orgId: string;
metadata: string;
}
export async function createNewLicense(orgId: string, licenseData: any): Promise<CreateNewLicenseResponse> {
export async function createNewLicense(
orgId: string,
licenseData: any
): Promise<CreateNewLicenseResponse> {
try {
const response = await fetch(
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/${orgId}/create`, // this says enterprise but it does both
@@ -80,7 +84,7 @@ export async function generateNewLicense(
next: NextFunction
): Promise<any> {
try {
const { orgId } = req.params;
const orgId = getFirstString(req.params.orgId);
if (!orgId) {
return next(

View File

@@ -16,6 +16,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { response as sendResponse } from "@server/lib/response";
import { getFirstString } from "@server/lib/requestParams";
import privateConfig from "#private/lib/config";
import {
GeneratedLicenseKey,
@@ -55,7 +56,7 @@ export async function listSaasLicenseKeys(
next: NextFunction
): Promise<any> {
try {
const { orgId } = req.params;
const orgId = getFirstString(req.params.orgId);
if (!orgId) {
return next(

View File

@@ -25,6 +25,7 @@ import { UserType } from "@server/types/UserTypes";
import { verifyPassword } from "@server/auth/password";
import { unauthorized } from "@server/auth/unauthorizedResponse";
import { verifyTotpCode } from "@server/auth/totp";
import { getFirstString } from "@server/lib/requestParams";
// The RP ID is the domain name of your application
const rpID = (() => {
@@ -406,7 +407,12 @@ export async function deleteSecurityKey(
res: Response,
next: NextFunction
): Promise<any> {
const { credentialId: encodedCredentialId } = req.params;
const encodedCredentialId = getFirstString(req.params.credentialId);
if (!encodedCredentialId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid credential ID")
);
}
const credentialId = decodeURIComponent(encodedCredentialId);
const user = req.user as User;

View File

@@ -8,7 +8,6 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { domain } from "zod/v4/core/regexes";
const getDomainSchema = z.strictObject({
domainId: z.string().optional(),

View File

@@ -19,6 +19,7 @@ import {
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { response } from "@server/lib/response";
import { getFirstString } from "@server/lib/requestParams";
export async function getUserResources(
req: Request,
@@ -26,7 +27,7 @@ export async function getUserResources(
next: NextFunction
): Promise<any> {
try {
const { orgId } = req.params;
const orgId = getFirstString(req.params.orgId);
const userId = req.user?.userId;
if (!userId) {
@@ -35,6 +36,12 @@ export async function getUserResources(
);
}
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
// Check user is in organization and get their role IDs
const [userOrg] = await db
.select()