Summary
In src/inngest/functions/process-pr-event.ts, the awardRecommendedMerge function passes rec.xp_reward directly from the database into insertXpEvent as the xpDelta. There is no validation against the defined tier caps in XP_REWARDS.RECOMMENDED_MERGE.
Affected File
src/inngest/functions/process-pr-event.ts
Root Cause
The insert call reads:
const inserted = await insertXpEvent({
userId: rec.user_id,
source: XP_SOURCE.RECOMMENDED_MERGE,
refType: 'pr',
refId: refIds.pr(repo, pr.number),
repo,
difficulty,
xpDelta: rec.xp_reward ?? xpForMerge(difficulty),
});
rec.xp_reward comes directly from a database row. The constants defined in src/lib/xp/sources.ts are:
export const XP_REWARDS = {
RECOMMENDED_MERGE: { E: 50, M: 150, H: 400 },
// ...
} as const;
These caps are never applied when rec.xp_reward is non-null. Any row in the recommendations table with an inflated xp_reward value (via direct database access, a compromised migration, or a future bug in a write path) will award that uncapped amount without any application-layer check.
Impact
- A
rec.xp_reward of 999999 for an Easy PR would be awarded as-is; the defined cap of 50 is silently bypassed.
- The idempotency guard (UNIQUE on
user_id, source, ref_id) prevents double-awarding the same PR, but it does not prevent a single inflated award from being committed.
- Leaderboard rankings and any downstream XP-gated features are directly affected.
Steps to Reproduce
- Insert a recommendations row with
xp_reward = 9999 and difficulty E.
- Merge a PR that matches the recommendation.
- Observe the awarded XP delta is
9999 rather than the expected cap of 50.
Expected Behaviour
xpDelta should be clamped to the tier ceiling before being persisted:
const tierCap = XP_REWARDS.RECOMMENDED_MERGE[difficulty as keyof typeof XP_REWARDS.RECOMMENDED_MERGE]
?? xpForMerge(difficulty);
const xpDelta = Math.min(rec.xp_reward ?? tierCap, tierCap);
Severity
High - This is a data-integrity issue in the reward pipeline. An uncapped write can corrupt leaderboard state and cannot be cleanly reversed once insertXpEvent succeeds (the idempotency key blocks a corrective re-award at the same refId).
Summary
In
src/inngest/functions/process-pr-event.ts, theawardRecommendedMergefunction passesrec.xp_rewarddirectly from the database intoinsertXpEventas thexpDelta. There is no validation against the defined tier caps inXP_REWARDS.RECOMMENDED_MERGE.Affected File
src/inngest/functions/process-pr-event.tsRoot Cause
The insert call reads:
rec.xp_rewardcomes directly from a database row. The constants defined insrc/lib/xp/sources.tsare:These caps are never applied when
rec.xp_rewardis non-null. Any row in the recommendations table with an inflatedxp_rewardvalue (via direct database access, a compromised migration, or a future bug in a write path) will award that uncapped amount without any application-layer check.Impact
rec.xp_rewardof999999for an Easy PR would be awarded as-is; the defined cap of 50 is silently bypassed.user_id, source, ref_id) prevents double-awarding the same PR, but it does not prevent a single inflated award from being committed.Steps to Reproduce
xp_reward = 9999and difficultyE.9999rather than the expected cap of50.Expected Behaviour
xpDeltashould be clamped to the tier ceiling before being persisted:Severity
High - This is a data-integrity issue in the reward pipeline. An uncapped write can corrupt leaderboard state and cannot be cleanly reversed once
insertXpEventsucceeds (the idempotency key blocks a corrective re-award at the samerefId).