Selected work / Security audit
Cybersecurity
case study:
Pawsi.
I identified and remediated two vulnerabilities in my application’s messaging system. I prepared a lab, tested the changes and checked their operation in production.

- My role
- Development, audit and remediation
- Case study scope
- Two messaging vulnerabilities
- Product
- Web and Android with Capacitor
- Security review
- October 2026
Pawsi’s background
and development journey
I created Pawsi to help reunite pets with their families through missing-pet posts, sightings and adoption listings. I launched the web application in September 2025, and its web MVP won the Hackathon del Milagro in Salta.
Web application launch
The application went live and welcomed its first users.
Android release
Published on Google Play using Capacitor and a shared web codebase.
Security audit and improvements
Local lab, selective recovery and messaging security fixes.
Sharing the story mattered, too.
I took part in more than 30 interviews to present the project. Explaining how it worked to nontechnical audiences was part of building it. The application remains operational, with less activity than at its initial peak.
Project press archive and campaign artwork. Images can be enlarged. Original media coverage is in Spanish.
Understand how
the pieces connect.
The interface uses React and TypeScript, with Capacitor for Android distribution. Supabase provides authentication, PostgreSQL and backend functions. Cloudflare R2 and its CDN serve images, while Firebase delivers push notifications.
Media has a one-year TTL in Cloudflare and the browser. Replacing or removing an image requires considering existing copies: purging the CDN does not clear browser caches.
The database and files require separate recovery procedures. Restoring a record containing an image URL does not recover the file it points to.
A place to test
before making changes.
I set up local Supabase with Docker and WSL and connected the frontend using the local environment’s credentials. External integrations were disabled or replaced during testing.
The initial startup revealed a specific problem: the migrations tried to alter a table that did not yet exist. I rebuilt the environment through a selective PostgreSQL backup restore and compared table and record counts.
Partial recovery of schema and data. Subsequent tests used synthetic accounts, and I checked that no test records remained.
Stored files, sessions and complete provider configuration. The exercise does not demonstrate full recovery or establish measured RTO or RPO values.
Having a backup and understanding what it can recover are two different tasks.
Unauthorized
notification requests
The previous flow let the client request notifications without adequately validating authorization to send them. I moved that decision to an event generated by the database when a valid message is saved.
- The authenticated client saves a message subject to database access controls.
- A trigger creates an in-app notification and a private event identifying its recipient.
- The database signs a request using HMAC and a timestamp.
- The Edge Function validates the signature and claims the event atomically.
- When delivery is allowed, it calls Firebase and records the outcome.
The old endpoint preserves compatibility with the published application but no longer sends notifications. An Android update was not required.
Preventing duplicates and bursts
I added event expiration, rate limits and one delivery attempt per event. Enabling delivery does not process historical messages. Events exceeding the limits are skipped instead of accumulating for later delivery.
View the 23 lab checks
Results recorded on October 20, 2026, using local Auth, PostgreSQL, pg_net and Edge Runtime, with a simulated push provider.
- Installing or reapplying the change does not notify about earlier messagesPASS
- An INSERT creates an in-app notification even when push is disabledPASS
- Enabling push does not process message history or disabled eventsPASS
- An INSERT from the existing client triggers a signed webhook and one pushPASS
- 20 calls to the old endpoint with fabricated identities produce no notificationsPASS
- Marking a message as read or editing it does not trigger new notificationsPASS
- Repeating an INSERT with the same ID does not duplicate notificationsPASS
- Replaying a delivered webhook does not duplicate the pushPASS
- No signature, a user JWT or a public key: 401PASS
- Tampered signature: 401PASS
- A signature for one event cannot authorize another: 401PASS
- Signature older than two minutes: 401PASS
- A user cannot claim or finalize events through RPCPASS
- A burst of 15 messages to the same recipient adds no push notificationsPASS
- The global limit skips delivery without creating a retry queuePASS
- Provider failure preserves the message and in-app notificationPASS
- Provider failure followed by a repeated request does not resendPASS
- 10 concurrent claims produce one delivery using the original event snapshotPASS
- An old event with a fresh signature is discarded without deliveryPASS
- A configuration failure does not prevent saving the message or notificationPASS
- A notification INSERT failure does not roll back the message and is loggedPASS
- Rolling back a message leaves no notification or eventPASS
- Reapplying the patch does not scan existing messagesPASS
Production checks
During deployment, a 503 response revealed that the secret’s name did not match what the function expected. After correcting the configuration, the notification reached Android. An unsigned request received 401 Unauthorized.
One attempt reduces duplication, but a failure after claiming the event can lose the push notification. The in-app notification and push delivery are separate mechanisms.
Excessive permissions
to update messages
The RLS policy allowed the recipient to update a message to mark it as read. However, the SQL UPDATE permission covered every column, allowing changes to the subject or content as well.
I reproduced a subject change in the lab, revoked the broad permission and granted UPDATE only on read_at. RLS still determines which messages each person can modify.
| Field | Before | After |
|---|---|---|
| Subject and content | Allowed | Denied |
| Identifiers and other timestamps | Allowed | Denied |
| Read status | Allowed | Allowed |
View the 15 permission checks
Results recorded on October 23, 2026, using synthetic accounts. The first check reproduces the excessive permission before remediation.
- Before the change: the recipient could edit the subjectPASS
- The recipient can mark the message as readPASS
- UPDATE denied: idPASS
- UPDATE denied: sender_idPASS
- UPDATE denied: recipient_idPASS
- UPDATE denied: post_idPASS
- UPDATE denied: post_typePASS
- UPDATE denied: subjectPASS
- UPDATE denied: contentPASS
- UPDATE denied: created_atPASS
- UPDATE denied: updated_atPASS
- RLS blocks the sender from marking the message as readPASS
- RLS blocks a third party from marking the message as readPASS
- Anonymous access has no UPDATE permissionPASS
- Marking a message as read creates no additional eventsPASS
In production, I checked effective permissions and confirmed that read status persisted between two accounts. The frontend already updated only that field, so no client changes were needed.
RLS restricts rows.
Privileges restrict columns.
From an intention
to a verifiable control.
The product’s rules needed to be enforced in the backend: who can modify a message, which fields they can change and which event authorizes a notification.
I documented the problems, decisions, tests and their limits. This case presents the identification and remediation of two messaging vulnerabilities, along with partial recovery in a lab. These checks do not constitute a security certification of the entire application.
Take the case study with you.
Background, architecture, decisions and results in a three-page document.
Contact details
Available for remote or on-site work and willing to travel. We can connect in Spanish or English.
Phone +543875044912diegopuzio@gmail.com