Skip to content

[Bug]: Interview experience idempotencyKey dedup ignores the author - any re-use of a key returns the first submitter's full submission #1795

Description

@ionfwsrijan

Description

POST /api/interview-experiences deduplicates submissions by idempotencyKey alone, with no scope to the submitting user or client key. Anyone who re-uses a key gets the first submitter's entire submission back.

backend/controllers/interviewExperienceController.js:73-80:

const existing = await InterviewExperience.findOne({ idempotencyKey });
if (existing) {
  return res.status(200).json({
    success: true,
    message: "Interview experience already submitted",
    experience: toClientShape(existing),   // full submission: company, role, rounds, tips, summary...
  });
}

The same unscoped lookup runs again on the unique-index race at interviewExperienceController.js:91-102.

The key is client-supplied (the frontend generates it at frontend/src/pages/InterviewExperiences/InterviewExperiences.jsx:528-530; the validator only requires 8-64 characters, see backend/Input_validators/ValidateInterviewExperience.js), and the route is public with optional auth, so the key is a predictable string like submit-<timestamp>-<random>.

Expected Behavior

Idempotency dedup should only return a match for the same author (same userId when authenticated, or same clientKey for anonymous submissions). A different user re-using a key should get their own fresh submission, not someone else's data.

Actual Behavior

  • User A submits an experience with idempotencyKey = "X". User B (or an anonymous visitor with any valid clientKey) POSTs with the same key "X" — maybe by simply copying the pattern, a stale retry from a shared browser, or a replayed request.
  • The controller finds User A's document via the global findOne({ idempotencyKey }) and returns 200 with User A's full submission (toClientShape includes company, role, experience, difficulty, rounds, summary, tips).
  • User B never gets their submission created (the 11000 branch at :91 has the same flaw), so their content is silently lost.
  • Because the key is guessable (submit- + timestamp + 36-base random), this is reachable without any special privileges.

Proposed Fix

  • Scope the dedup to the author in both places. For example:
const authorFilter = req.user?._id
  ? { userId: req.user._id }
  : { clientKey: payload.clientKey };

const existing = await InterviewExperience.findOne({
  idempotencyKey,
  ...authorFilter,
});
  • Mirror the same filter in the 11000 race branch (interviewExperienceController.js:91-102).
  • Optionally tighten the validator to require a format that is not predictable (e.g. at least one cryptographically random segment), since the current submit-<Date.now()>-<Math.random> fallback is guessable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions