From e9b5fd7a81655ba75ec7c21f6eb5ee5ce938cda8 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sat, 19 Jul 2025 12:29:55 +0200 Subject: [PATCH] fix some attack vectors --- .../src/graphql/resolver/CommunityResolver.ts | 2 + database/src/entity/Community.ts | 4 + .../1_0/resolver/AuthenticationResolver.ts | 204 ++++++++++-------- .../api/1_0/util/authenticateCommunity.ts | 9 +- shared/src/schema/base.schema.ts | 5 +- 5 files changed, 133 insertions(+), 91 deletions(-) diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index 779e7db31..16130f5d4 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -38,6 +38,7 @@ export class CommunityResolver { @Authorized([RIGHTS.COMMUNITIES]) @Query(() => [AdminCommunityView]) async allCommunities(@Args() paginated: Paginated): Promise { + // communityUUID could be oneTimePassCode (uint32 number) return (await getAllCommunities(paginated)).map((dbCom) => new AdminCommunityView(dbCom)) } @@ -58,6 +59,7 @@ export class CommunityResolver { async communityByIdentifier( @Arg('communityIdentifier') communityIdentifier: string, ): Promise { + // communityUUID could be oneTimePassCode (uint32 number) const community = await getCommunityByIdentifier(communityIdentifier) if (!community) { throw new LogError('community not found', communityIdentifier) diff --git a/database/src/entity/Community.ts b/database/src/entity/Community.ts index b5e3f0ce4..bee4d5fa8 100644 --- a/database/src/entity/Community.ts +++ b/database/src/entity/Community.ts @@ -30,6 +30,10 @@ export class Community extends BaseEntity { @Column({ name: 'private_key', type: 'binary', length: 64, nullable: true }) privateKey: Buffer | null + /** + * Most of time a uuidv4 value, but could be also a uint32 number for a short amount of time, so please check before use + * in community authentication this field is used to store a oneTimePassCode (uint32 number) + */ @Column({ name: 'community_uuid', type: 'char', diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index bfec30bc5..3c62c7bef 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -9,11 +9,11 @@ import { getHomeCommunity, } from 'database' import { getLogger } from 'log4js' -import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, OpenConnectionJwtPayloadType } from 'shared' +import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, OpenConnectionJwtPayloadType, uint32Schema, uuidv4Schema } from 'shared' import { Arg, Mutation, Resolver } from 'type-graphql' import { startAuthentication, startOpenConnectionCallback } from '../util/authenticateCommunity' -const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver`) +const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.${method}`) @Resolver() export class AuthenticationResolver { @@ -22,45 +22,49 @@ export class AuthenticationResolver { @Arg('data') args: EncryptedTransferArgs, ): Promise { - const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.openConnection`) + const methodLogger = createLogger('openConnection') methodLogger.addContext('handshakeID', args.handshakeID) methodLogger.debug(`openConnection() via apiVersion=1_0:`, args) - const openConnectionJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionJwtPayloadType - methodLogger.debug('openConnectionJwtPayload', openConnectionJwtPayload) - if (!openConnectionJwtPayload) { - const errmsg = `invalid OpenConnection payload of requesting community with publicKey` + args.publicKey - methodLogger.error(errmsg) - methodLogger.removeContext('handshakeID') - throw new Error(errmsg) - } - if (openConnectionJwtPayload.tokentype !== OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE) { - const errmsg = `invalid tokentype of community with publicKey` + args.publicKey - methodLogger.error(errmsg) - methodLogger.removeContext('handshakeID') - throw new Error(errmsg) - } - if (!openConnectionJwtPayload.url) { - const errmsg = `invalid url of community with publicKey` + args.publicKey - methodLogger.error(errmsg) - methodLogger.removeContext('handshakeID') - throw new Error(errmsg) - } - methodLogger.debug(`vor DbFedCommunity.findOneByOrFail()...`, { publicKey: args.publicKey }) - const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: Buffer.from(args.publicKey, 'hex') }) - methodLogger.debug(`nach DbFedCommunity.findOneByOrFail()...`, fedComA) - methodLogger.debug('fedComA', new FederatedCommunityLoggingView(fedComA)) - if (!openConnectionJwtPayload.url.startsWith(fedComA.endPoint)) { - const errmsg = `invalid url of community with publicKey` + args.publicKey - methodLogger.error(errmsg) - methodLogger.removeContext('handshakeID') - throw new Error(errmsg) - } + try { + const openConnectionJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionJwtPayloadType + methodLogger.debug('openConnectionJwtPayload', openConnectionJwtPayload) + if (!openConnectionJwtPayload) { + const errmsg = `invalid OpenConnection payload of requesting community with publicKey` + args.publicKey + methodLogger.error(errmsg) + // no infos to the caller + return true + } + if (openConnectionJwtPayload.tokentype !== OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE) { + const errmsg = `invalid tokentype of community with publicKey` + args.publicKey + methodLogger.error(errmsg) + // no infos to the caller + return true + } + if (!openConnectionJwtPayload.url) { + const errmsg = `invalid url of community with publicKey` + args.publicKey + methodLogger.error(errmsg) + // no infos to the caller + return true + } + methodLogger.debug(`vor DbFedCommunity.findOneByOrFail()...`, { publicKey: args.publicKey }) + const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: Buffer.from(args.publicKey, 'hex') }) + methodLogger.debug(`nach DbFedCommunity.findOneByOrFail()...`, fedComA) + methodLogger.debug('fedComA', new FederatedCommunityLoggingView(fedComA)) + if (!openConnectionJwtPayload.url.startsWith(fedComA.endPoint)) { + const errmsg = `invalid url of community with publicKey` + args.publicKey + methodLogger.error(errmsg) + // no infos to the caller + return true + } - // no await to respond immediately and invoke callback-request asynchronously - void startOpenConnectionCallback(args.handshakeID, args.publicKey, CONFIG.FEDERATION_API) - methodLogger.debug('openConnection() successfully initiated callback and returns true immediately...') - methodLogger.removeContext('handshakeID') - return true + // no await to respond immediately and invoke callback-request asynchronously + void startOpenConnectionCallback(args.handshakeID, args.publicKey, CONFIG.FEDERATION_API) + methodLogger.debug('openConnection() successfully initiated callback and returns true immediately...') + return true + } catch (err) { + methodLogger.error('invalid jwt token:', err) + return true + } } @Mutation(() => Boolean) @@ -68,37 +72,41 @@ export class AuthenticationResolver { @Arg('data') args: EncryptedTransferArgs, ): Promise { - const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.openConnectionCallback`) + const methodLogger = createLogger('openConnectionCallback') methodLogger.addContext('handshakeID', args.handshakeID) methodLogger.debug(`openConnectionCallback() via apiVersion=1_0 ...`, args) + try { // decrypt args.url with homeCom.privateJwtKey and verify signing with callbackFedCom.publicKey - const openConnectionCallbackJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionCallbackJwtPayloadType - if (!openConnectionCallbackJwtPayload) { - const errmsg = `invalid OpenConnectionCallback payload of requesting community with publicKey` + args.publicKey - methodLogger.error(errmsg) - methodLogger.removeContext('handshakeID') - throw new Error(errmsg) - } + const openConnectionCallbackJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionCallbackJwtPayloadType + if (!openConnectionCallbackJwtPayload) { + const errmsg = `invalid OpenConnectionCallback payload of requesting community with publicKey` + args.publicKey + methodLogger.error(errmsg) + // no infos to the caller + return true + } - const endPoint = openConnectionCallbackJwtPayload.url.slice(0, openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1) - const apiVersion = openConnectionCallbackJwtPayload.url.slice(openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1, openConnectionCallbackJwtPayload.url.length) - methodLogger.debug(`search fedComB per:`, endPoint, apiVersion) - const fedComB = await DbFedCommunity.findOneBy({ endPoint, apiVersion }) - if (!fedComB) { - const errmsg = `unknown callback community with url` + openConnectionCallbackJwtPayload.url - methodLogger.error(errmsg) - methodLogger.removeContext('handshakeID') - throw new Error(errmsg) + const endPoint = openConnectionCallbackJwtPayload.url.slice(0, openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1) + const apiVersion = openConnectionCallbackJwtPayload.url.slice(openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1, openConnectionCallbackJwtPayload.url.length) + methodLogger.debug(`search fedComB per:`, endPoint, apiVersion) + const fedComB = await DbFedCommunity.findOneBy({ endPoint, apiVersion }) + if (!fedComB) { + const errmsg = `unknown callback community with url` + openConnectionCallbackJwtPayload.url + methodLogger.error(errmsg) + // no infos to the caller + return true + } + methodLogger.debug( + `found fedComB and start authentication:`, + new FederatedCommunityLoggingView(fedComB), + ) + // no await to respond immediately and invoke authenticate-request asynchronously + void startAuthentication(args.handshakeID, openConnectionCallbackJwtPayload.oneTimeCode, fedComB) + methodLogger.debug('openConnectionCallback() successfully initiated authentication and returns true immediately...') + return true + } catch (err) { + methodLogger.error('invalid jwt token:', err) + return true } - methodLogger.debug( - `found fedComB and start authentication:`, - new FederatedCommunityLoggingView(fedComB), - ) - // no await to respond immediately and invoke authenticate-request asynchronously - void startAuthentication(args.handshakeID, openConnectionCallbackJwtPayload.oneTimeCode, fedComB) - methodLogger.debug('openConnectionCallback() successfully initiated authentication and returns true immediately...') - methodLogger.removeContext('handshakeID') - return true } @Mutation(() => String) @@ -106,32 +114,54 @@ export class AuthenticationResolver { @Arg('data') args: EncryptedTransferArgs, ): Promise { - const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.authenticate`) + const methodLogger = createLogger('authenticate') methodLogger.addContext('handshakeID', args.handshakeID) methodLogger.debug(`authenticate() via apiVersion=1_0 ...`, args) - const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType - if (!authArgs) { - const errmsg = `invalid authentication payload of requesting community with publicKey` + args.publicKey - methodLogger.error(errmsg) - methodLogger.removeContext('handshakeID') - throw new Error(errmsg) - } - const authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode }) - methodLogger.debug('found authCom:', new CommunityLoggingView(authCom)) - if (authCom) { - authCom.communityUuid = authArgs.uuid - authCom.authenticatedAt = new Date() - await DbCommunity.save(authCom) - methodLogger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom)) - const homeComB = await getHomeCommunity() - if (homeComB?.communityUuid) { - const responseArgs = new AuthenticationResponseJwtPayloadType(args.handshakeID,homeComB.communityUuid) - const responseJwt = await encryptAndSign(responseArgs, homeComB.privateJwtKey!, authCom.publicJwtKey!) - methodLogger.removeContext('handshakeID') - return responseJwt + try { + const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType + if (!authArgs) { + const errmsg = `invalid authentication payload of requesting community with publicKey` + args.publicKey + methodLogger.error(errmsg) + // no infos to the caller + return null } + if (!uint32Schema.safeParse(authArgs.oneTimeCode).success) { + const errmsg = `invalid oneTimeCode: ${authArgs.oneTimeCode} for community with publicKey ${authArgs.publicKey}, expect uint32` + methodLogger.error(errmsg) + // no infos to the caller + return null + } + const authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode }) + if (authCom) { + methodLogger.debug('found authCom:', new CommunityLoggingView(authCom)) + if (authCom.publicKey !== authArgs.publicKey) { + const errmsg = `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${authArgs.publicKey}` + methodLogger.error(errmsg) + // no infos to the caller + return null + } + const communityUuid = uuidv4Schema.safeParse(authArgs.uuid) + if (!communityUuid.success) { + const errmsg = `invalid uuid: ${authArgs.uuid} for community with publicKey ${authArgs.publicKey}` + methodLogger.error(errmsg) + // no infos to the caller + return null + } + authCom.communityUuid = communityUuid.data + authCom.authenticatedAt = new Date() + await DbCommunity.save(authCom) + methodLogger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom)) + const homeComB = await getHomeCommunity() + if (homeComB?.communityUuid) { + const responseArgs = new AuthenticationResponseJwtPayloadType(args.handshakeID,homeComB.communityUuid) + const responseJwt = await encryptAndSign(responseArgs, homeComB.privateJwtKey!, authCom.publicJwtKey!) + return responseJwt + } + } + return null + } catch (err) { + methodLogger.error('invalid jwt token:', err) + return null } - methodLogger.removeContext('handshakeID') - return null } } diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index c51546c15..33f725737 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -14,7 +14,7 @@ import { randombytes_random } from 'sodium-native' import { AuthenticationClient as V1_0_AuthenticationClient } from '@/client/1_0/AuthenticationClient' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' -import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, verifyAndDecrypt } from 'shared' +import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, uuidv4Schema, verifyAndDecrypt } from 'shared' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity`) @@ -40,8 +40,13 @@ export async function startOpenConnectionCallback( apiVersion: api, publicKey: comA.publicKey, }) - const oneTimeCode = randombytes_random().toString() // store oneTimeCode in requestedCom.community_uuid as authenticate-request-identifier + // prevent overwriting valid UUID with oneTimeCode, because this request could be initiated at any time from federated community + if (uuidv4Schema.safeParse(comA.communityUuid).success) { + throw new Error('Community UUID is already a valid UUID') + } + // TODO: make sure it is unique + const oneTimeCode = randombytes_random().toString() comA.communityUuid = oneTimeCode await DbCommunity.save(comA) methodLogger.debug( diff --git a/shared/src/schema/base.schema.ts b/shared/src/schema/base.schema.ts index ed341c48b..ee9383dd2 100644 --- a/shared/src/schema/base.schema.ts +++ b/shared/src/schema/base.schema.ts @@ -1,6 +1,7 @@ -import { string } from 'zod' +import { string, number } from 'zod' import { validate, version } from 'uuid' export const uuidv4Schema = string().refine((val: string) => validate(val) && version(val) === 4, 'Invalid uuid') export const emailSchema = string().email() -export const urlSchema = string().url() \ No newline at end of file +export const urlSchema = string().url() +export const uint32Schema = number().positive().lte(4294967295) \ No newline at end of file