Skip to content

Conversation

@hdiniz
Copy link
Contributor

@hdiniz hdiniz commented Dec 15, 2025

if (payeeCollective?.type === CollectiveType.USER) {
const kycVerifications = await Promise.all(
Object.values(KYCProviderName).map(provider =>
req.loaders.KYCVerification.verifiedStatusByProvider(expense.HostCollectiveId, provider).load(

This comment was marked as outdated.

@hdiniz hdiniz requested a review from kewitz December 16, 2025 20:12
Copy link
Contributor

@kewitz kewitz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please double-check relying on expense.HostCollectiveId for security checks on pending expenses.

data: {
legalAddress: providerRequest.legalAddress,
legalName: providerRequest.legalName,
...(providerRequest.legalAddress ? { legalAddress: providerRequest.legalAddress } : {}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a suggestion, Object.assign accepts false arguments:

data: Object.assign({ legalName: providerRequest.legalName }, providerRequest.legalAddress && { legalAddress: providerRequest.legalAddress })

In this particular situation pick(providerRequest, ['legalName', 'legalAddress']) would also have the same effect.

if (payeeCollective?.type === CollectiveType.USER) {
const kycVerifications = await Promise.all(
Object.values(KYCProviderName).map(provider =>
req.loaders.KYCVerification.verifiedStatusByProvider(expense.HostCollectiveId, provider).load(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how the code stands now, but please review exactly in which conditions expense.HostCollectiveId is populated. It used to be that it was populated only after the expense was paid, to cover for cases where the collective would change hosts with existing pending expenses.

Comment on lines +508 to +510
expense.fromCollective || (await req.loaders.Collective.byId.load(expense.FromCollectiveId));
if (payeeCollective?.type === CollectiveType.USER) {
const kycVerifications = await Promise.all(

This comment was marked as outdated.

if (payeeCollective?.type === CollectiveType.USER) {
const kycVerifications = await Promise.all(
Object.values(KYCProviderName).map(provider =>
req.loaders.KYCVerification.verifiedStatusByProvider(expense.HostCollectiveId, provider).load(

This comment was marked as outdated.

Comment on lines +506 to +514
const hostCollectiveId = expense.HostCollectiveId || expense.collective?.HostCollectiveId;

// KYC Verification Check: only for individual payees
const payeeCollective =
expense.fromCollective || (await req.loaders.Collective.byId.load(expense.FromCollectiveId));
if (payeeCollective?.type === CollectiveType.USER) {
const kycVerifications = await Promise.all(
Object.values(KYCProviderName).map(provider =>
req.loaders.KYCVerification.verifiedStatusByProvider(hostCollectiveId, provider).load(payeeCollective.id),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: hostCollectiveId can be undefined if an expense has no host. This causes the KYC verification loader to receive undefined and silently fail, bypassing the check.
Severity: CRITICAL | Confidence: High

🔍 Detailed Analysis

The hostCollectiveId variable can become undefined if both expense.HostCollectiveId and expense.collective?.HostCollectiveId are null. This undefined value is then passed to req.loaders.KYCVerification.verifiedStatusByProvider. The underlying SQL query will use NULL for the RequestedByCollectiveId, which will not match any records because NULL = NULL is not true in SQL. As a result, the kycVerifications array will contain only null values, and the subsequent loop will silently skip all KYC checks, potentially allowing payments to unverified individuals.

💡 Suggested Fix

Add a check to ensure hostCollectiveId is a valid number before calling req.loaders.KYCVerification.verifiedStatusByProvider. If hostCollectiveId is not available, either throw an error or handle the case explicitly to prevent the silent failure of the KYC check.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: server/lib/security/expense.ts#L506-L514

Potential issue: The `hostCollectiveId` variable can become `undefined` if both
`expense.HostCollectiveId` and `expense.collective?.HostCollectiveId` are null. This
`undefined` value is then passed to
`req.loaders.KYCVerification.verifiedStatusByProvider`. The underlying SQL query will
use `NULL` for the `RequestedByCollectiveId`, which will not match any records because
`NULL = NULL` is not true in SQL. As a result, the `kycVerifications` array will contain
only `null` values, and the subsequent loop will silently skip all KYC checks,
potentially allowing payments to unverified individuals.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 8181271

@hdiniz hdiniz merged commit c0583b7 into main Jan 7, 2026
24 of 25 checks passed
@hdiniz hdiniz deleted the kyc-followups branch January 7, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Integrate KYC status on expense security checks

3 participants