Skip to content

Finding Flaws, Building Trust: My UoS E-Parking Experience

CybersecurityWebNext.js

7 min read


Mohammad Hajjiri

Written by / Mohammad Hajjiri

A screenshot of the UoS Parkings app interface with a Post Parking Signal button.

On the 15th of September, 2025, Ziad Aloush has launched UoS E-Parking to address the chaotic reality of campus parking; finding parking at the University of Sharjah campus is often more stressful than the classes themselves. 🧍🏼

Built in just one week, UoS E-Parking is a bilingual, mobile-first web app that lets university students instantly post when they’re leaving, mark a spot as free, or request a spot near a specific building within university campus.

Launched in the morning, the platform spread quickly — within 12 hours, hundreds of students had already used it, and it generated thousands of page views, showing just how much demand there was for a tool like this.

TL;DR (Executive Summary)

  • A vulnerability in the early UoS E-Parking client/server design allowed client-supplied device identifiers (user_hash) to be trusted by the backend for authorization, rather than being validated server-side.
  • Since GET /api/signals returned id + user_hash for each post, an attacker could harvest those values and call DELETE /api/signals or PATCH /api/signals with another user’s userHash. In practice, this opened the door to unauthorized deletions, forced updates, and other IDOR-like manipulations of posts that did not belong to them.
  • The vulnerability was responsibly reported to the developer (Ziad), who responded with impressive speed. Within hours, fixes were shipped that introduced session-based authentication, stricter server-side validation, and sanitized responses that no longer leaked sensitive identifiers, too.
  • This write-up documents what I found, how I tested it responsibly, why this class of issue is dangerous, and the kinds of code-level fixes that were applied or could be applied in other similar scenarios.

🔴 Note (Post-Fix Update): All of the vulnerabilities described below have since been patched; the API no longer accepts client-supplied userHash.

⚠️ Important: All responses have been sanitized, and authentication with session-based validation is now enforced. All of the examples shown in this blog post are strictly for documenting the discovery process (under Ziad's permission) — they no longer work on the current, fixed version of the system.

Background: Launch of UoS E-Parking

The initial deployment of UoS E-Parking was simple but effective; the app was built on a stack of Next.js + PostgreSQL + Tailwind, deployed on Vercel. Its UX was designed to be mobile-first, polling GET /api/signals roughly every 20 seconds, with signals configured to auto-expire after a set time.

I began exploring the app out of curiosity; I was digging into the client bundle to see how it worked. Using the Safari console, I confirmed that GET /api/signals returned an array of objects containing not only the id of each signal but also the user_hash, along with some additional metadata.

Further inspection showed that the client relied on localStorage to manage identity. Specifically, it stored a value called uos-parking-device-hash, which the app treated as the user’s identity, and later passed directly to the server.

I then tested DELETE /api/signals by sending a request body like { signalId, userHash }. To my surprise, the server accepted this unverified userHash as proof of ownership and deleted the post. To confirm this wasn’t a one-off, I repeated the test across devices, spoofing values in localStorage on my iPhone and desktop, and saw the same results.

After documenting everything, I wrote a short responsible disclosure and sent it privately to Ziad. He responded quickly, acknowledged the flaw, and within hours shipped fixes: session-based authentication, server-side validation, and sanitized API responses. Later, he even shared a public acknowledgement on LinkedIn. This post now explains the technical details behind my discovery:

What's the root cause of the issue?

There were three key issues that came together to create the vulnerability.

First, the client supplied its own identity; it generated & stored a user_hash in localStorage using a helper function (u()), and included this value (p_user_hash or userHash) in requests to create/update/delete signals:

JS
function u() {
    let e = localStorage.getItem("uos-parking-device-hash");
    return e || (e = "uos_" + Math.random().toString(36).substr(2, 15) + Date.now().toString(36),
    localStorage.setItem("uos-parking-device-hash", e)), e
};

The problem is obvious:

  • localStorage is fully under client control; anyone can overwrite this value manually and pretend to be another user.
  • The server trusted the userHash sent in the request body; instead of validating ownership on the server side, it simply accepted whatever the client reported as legitimate.
  • The API leaked information; GET /api/signals returned both the id and the user_hash for every signal, meaning attackers could harvest pairs of identifiers and then use them to perform destructive actions.

These 3 issues made impersonation & abuse trivial — and even scriptable.

Why is this dangerous as an attacker?

In order to see why this mattered, imagine a low-effort attacker running a simple script. Every few seconds, the script polls GET /api/signals to collect all (id, user_hash) pairs for newly created posts. As soon as a new signal appears, the script calls DELETE /api/signals with { signalId, userHash } to remove it. In practice, this means every new signal that gets created disappears within seconds of being created. For students depending on the app, the feed would look broken, with posts vanishing almost instantly.

The scary part is that this didn’t require elevated privileges or advanced exploits; the server trusted userHash supplied by the client, anyone with minimal knowledge of JavaScript and a browser console could launch this denial-of-service style attack. However, the risks didn’t end there; many posts included phone numbers, which meant that by harvesting API responses, an attacker could also scrape user contact details at scale. Therefore, what started as a convenience feature quickly turned into a potential privacy risk.

Concrete fixes (what was done / what to do)

Below are pragmatic, prioritized fixes — with code where relevant.

A — Immediate Hotfixes (must deploy asap)

  • Strip user_hash from public responses: GET /api/signals should not return user_hash for arbitrary viewers. Instead, return it only when the row belongs to the requesting, authenticated user. For example, (pseudo):
JS
res.json((await db.signals.findMany(...)).map(row => {
    if (row.user_hash === currentDeviceHash) return row;
    const { user_hash, ...rest } = row;
    return rest; // Hide user_hash.
}));
  • Stop accepting userHash from request bodies for auth: Ignore any userHash that are client-supplied in POST / PATCH / DELETE.

B - Short-Term (Better) Solution: Server-Signed Device Token

If you don’t have a full user account system yet, implement a signed device token (HMAC) issued by the server and stored in an HttpOnly cookie:

For example (pseudo), an issue on the first contact (server):

JS
// signDevice(hash) -> `${hash}.${hmac(hash, SECRET)}`
res.cookie('device', token, { httpOnly: true, sameSite: 'Lax', secure: true });

You'll verify on each request:

JS
const token = req.cookies.device;
const verifiedHash = verifyDevice(token); // Returns null if invalid.
// Use verifiedHash as server-trusted device identity.

Use verifiedHash for ownership checks and for assigning user_hash when creating signals.

Response Disclosure & Coordination

I contacted Ziad privately and provided the PoC and reproduction steps. He responded and fixed quickly — that’s a perfect responsible-disclosure flow.

Huge thanks to him for the quick response and for treating the report seriously — shipping fixes within hours after the disclosure is exactly how secure, user-focused software should behave. You can checkout his portfolio @ ziad.us. :)

Edit on GitHub