hacking NA’s biggest hackathon

xtra
5 min read

hack the north is the first hackathon i’ve ever applied to. i got accepted, but NONE of my friends did. i really wanted to experience it with them, so i opened devtools and started looking at how the application system worked.

the network tab led me to the GraphQL API behind the site. i started following the operations used to manage applications and kept finding small permission issues. each one gave an applicant a little more control. after a few hours, they fit together into a chain that made rejection far less final than it should have been.

hack the north acceptance email preview saying congratulations, you’re in
my original hack the north acceptance email.

An official note from the Hack the North team:

The reported vulnerability has been fixed. No personal information was exposed, and we are actively monitoring our systems to ensure no one attends Hack the North unfairly through this or similar issues. Read our full response.

how the application system worked

the application used this GraphQL endpoint:

https://api.hackthenorth.com/v3/graphql

throughout this post, “claim” means the database record for an application.

each claim had an owner, a pipeline, a stage, a name, and its answers. the pipeline was the full application process, while the stage was the application’s current status inside it. the 2026 hacker application pipeline was called hackthenorth2026-applications.

inside that pipeline, not_accepted meant rejected, waitlisted meant placed on the waitlist, and accepted and auto-accepted were acceptance stages. after an accepted hacker completed the RSVP to confirm their spot, the claim moved to confirmed. checked_in came even later, after they arrived at the event.

this was the dashboard on my friend’s account before the chain:

hack the north dashboard telling an applicant they were not accepted
the rejected dashboard before any changes.

the GraphQL schema exposed four mutations that changed claim data. createCollaborator added another account as a collaborator, updateClaim edited an existing claim, createClaim created a new one, and deleteClaim removed one. two input fields mattered most: user_id controlled the claim’s owner, while stage_slug selected its stage using a name such as confirmed.

first, i tried the obvious approach: calling updateClaim on the rejected claim and replacing its stage with confirmed. i tried sending the confirmed slug, its numeric stage id, and a few different combinations of the update fields. every version was denied, and the claim stayed at not_accepted. the stage was being checked by the backend, so changing the request in devtools was not enough.

next, i tried leaving the rejected claim alone and creating a second one directly at confirmed. that request hit a different rule: each account could own only one claim in the pipeline, and the rejected claim still counted. the direct update was blocked by the stage check, while the new claim was blocked by the one-application limit.

both obvious routes were blocked. then i noticed that claims could have collaborators.

the collaborator bug

the collaborator feature let an applicant invite another normal account to help edit the same claim. in my test, my friend’s rejected account was the original account, and my own accepted account was the helper. the helper could edit the answers, while the original applicant remained the claim owner.

from the helper account, i could read the claim and call updateClaim. when i inspected the mutation input, i found that user_id was writable too. i sent an update that changed only that ownership field:

const data = {
id: REJECTED_CLAIM_ID,
user_id: HELPER_USER_ID,
}

the server checked that the helper could edit the claim, but it did not stop a collaborator from changing its owner. the mutation succeeded, and the same rejected claim now belonged to my accepted account. its name, answers, resume, and not_accepted stage all stayed the same.

my accepted account already owned its own claim in the same pipeline, so it now temporarily owned two: its accepted claim and my friend’s rejected claim. the one-application check existed in createClaim, but updateClaim did not repeat it when user_id changed.

the original account no longer owned that claim. as far as the pipeline’s one-application check was concerned, the account now had zero applications. the check that had blocked the second createClaim request would now pass.

creating a confirmed application

after transferring the rejected claim to the helper, i switched back to the original account and sent createClaim again. this time, the request reached the starting-stage checks.

i tested which stage a new claim could start at. accepted, auto-accepted, and waitlisted were rejected, while confirmed and checked_in passed the stage check. normally, confirmed came only after an accepted hacker completed their RSVP, so starting there skipped both the acceptance decision and the RSVP transition.

this path would not update the old rejected claim. it would create a separate database record already at confirmed, owned by the account sending the request.

the createClaim request used:

const data = {
name: originalClaim.name,
pipeline_slug: 'hackthenorth2026-applications',
stage_slug: 'confirmed',
answers: copiedAnswers,
}

pipeline_slug placed the new claim in the 2026 application pipeline, while stage_slug set its starting stage to confirmed. i copied the original name and answers so the new claim still represented the same applicant.

put together, the full chain looked like this:

swipe to see the helper account →
changing the owner made the original account look empty to the pipeline’s one-claim check, so it could create a new application directly at confirmed.

the final result

when i sent the complete request, the API stored the new claim at confirmed.

the old rejected claim still existed at not_accepted under my accepted account, alongside my own claim. to remove it, i used my accepted account to add my friend as a collaborator on that old claim. i then switched back to my friend’s account and called deleteClaim. this deleted the rejected record while leaving my own claim untouched.

fresh production queries showed:

  • exactly one claim on my friend’s account at confirmed, with the original name, answers, and resume
  • no collaborator links or old rejected claim
  • my accepted account back to its original claim

refreshing my friend’s dashboard replaced the rejection page shown earlier with the RSVP screen:

hack the north dashboard showing RSVP confirmed and an edit RSVP submission button
the same account now appeared as RSVP confirmed.

my friend filled in the missing RSVP questions and submitted them. the site then generated a hacker ticket:

hack the north 2026 hacker ticket with personal details redacted

the generated ticket, with personal details redacted.

timeline

  • august 12, 2026 - discovered the full vulnerability chain.
  • august 18, 2026 - initial patches deployed by HTN.
  • august 20, 2026 - i retested the patches.
  • august 27, 2026 - final patches deployed by HTN.