I Found a Security Vulnerability on My Own Website. Here Is What Happened.
By Michael Andrews | Published: 2026-08-24 | Updated: 2026-08-24 | 7 min read
During a routine database audit on 23 August 2026, I discovered that my own course certificate system had a row-level security policy that let anyone insert a fake certificate into the database. A second vulnerability in the professional exam table had the same problem. Both are now fixed. Nobody was affected. This is the full disclosure.
On 23 August 2026, during a routine audit of my database security policies, I discovered that anyone with basic technical knowledge could forge a course completion certificate on this website. A second vulnerability in the professional exam system had the same exposure. Both are now fixed. Nobody was affected. This is the full disclosure.
I am publishing this because I believe transparency matters more than appearances. I advise people on technical SEO and web security. If I find a problem on my own site and quietly fix it, that is not the standard I want to hold myself to.
What I found
This site uses Supabase as its database backend. Supabase exposes a REST API secured by row-level security (RLS) policies. Those policies determine what any given user can read, write, update or delete.
The course certificate system was originally built with an INSERT policy that validated the recipient name format and length, but did not verify whether the person had actually completed the course. The validation checked that the name was between 2 and 100 characters and matched a reasonable character pattern. That is all.
In practice, this meant anyone who knew the table structure and the public API key (which is visible in the browser by design in Supabase) could insert a row claiming they had completed the Wix SEO Course with a perfect score. The certificate verification page would then confirm it as genuine.
The second vulnerability
While auditing the first issue, I checked every other table. The professional_exam_passes table, which records when someone passes the paid professional certification exam, had fully open INSERT, UPDATE, and DELETE policies. The policies used WITH CHECK (true) and USING (true), which is database shorthand for "allow everything, no questions asked".
This was caught during the same audit and fixed before anyone used the exam system. The table had zero rows. Nobody was affected.
How I confirmed it
I tested the certificate vulnerability by sending a direct API request to insert a forged record. It succeeded immediately. The record appeared in the database with a fabricated certificate ID, a fake name, and a perfect score. The verification page confirmed it as valid.
That forged record was the only illegitimate entry in the table. I have since deleted it along with a second test record I created while building the fix.
The timeline
| Date | Event |
|---|---|
| 23 August 2026 | Routine database security audit begins |
| 23 August 2026 | Certificate INSERT policy vulnerability identified |
| 23 August 2026 | Forged test record inserted to confirm exploit |
| 23 August 2026 | Fix deployed: all write policies removed from course_certificates |
| 23 August 2026 | Professional exam passes vulnerability identified during same audit |
| 24 August 2026 | Fix deployed: all write policies removed from professional_exam_passes |
| 24 August 2026 | Forged test records deleted from database |
| 24 August 2026 | Forgery test repeated against both tables, confirmed rejected |
| 24 August 2026 | This disclosure published |
The root cause
When the certificate system was first created, it was designed with client-side writes in mind. The INSERT policy was added so the frontend could write certificate records directly after a quiz was completed. The validation in the policy was meant to be a guardrail.
Later, the architecture changed. Certificate issuance was moved to a server-side edge function that uses a privileged service role key, bypassing RLS entirely. The edge function validates quiz completion properly before writing anything. But the old permissive INSERT policy was never removed. It became a vestigial door that nobody was supposed to use, but anyone could.
The professional exam table was created with the same pattern: open policies as a development convenience, with the expectation that the edge function would handle all real writes. The policies should have been removed before the table went live.
What I fixed
- Removed all INSERT, UPDATE, and DELETE policies from course_certificates. The table now allows only SELECT for public verification lookups.
- Removed all INSERT, UPDATE, and DELETE policies from professional_exam_passes. Same pattern: SELECT only for validation.
- Both tables are now writable only by the server-side edge functions, which use a privileged key that bypasses row-level security.
- Deleted the forged test records from the database.
- Confirmed the fix by repeating the forgery attempt. Both tables now return a row-level security violation error on any INSERT attempt from the public API.
Who was affected
Nobody. The only forged record was mine, created during testing. The professional exam table had zero rows. No real certificates were fabricated by anyone else. No user data was exposed, accessed, or modified.
What I learned
A table that was harmless during development becomes dangerous the moment the data it holds has real-world value. A course certificate that people put on LinkedIn is worth forging. A permissive policy that was fine during prototyping becomes a vulnerability the day the system goes live.
The specific lesson: when you move from client-side writes to server-side writes, remove the client-side permissions. Do not leave them as a fallback. Do not assume that because the frontend no longer uses them, they are safe. The API is still there. The policy is still enforced. And someone with a cURL command can use what your frontend no longer does.
Expert Tip: If you use Supabase or any database with row-level security, run SELECT policyname, cmd, roles, qual, with_check FROM pg_policies WHERE tablename = 'your_table' and ask yourself: does every INSERT and UPDATE policy genuinely validate what it should? If the answer involves WITH CHECK (true) on a table that holds anything valuable, fix it now.
Why I am publishing this
Three reasons.
First, accountability. I found a problem. I fixed it. Saying so publicly is the honest thing to do, even when the problem is embarrassing.
Second, education. Row-level security misconfigurations are extremely common in Supabase applications. Most developers set permissive policies during development and forget to tighten them. This is a concrete example of what that looks like in production.
Third, trust. If I tell clients to audit their security configurations, I should demonstrate that I do the same to my own site, including when the results are unflattering.
Finding and fixing vulnerabilities is normal maintenance. It is not a scandal. Every application has them. The difference is whether you look for them, fix them promptly, and tell the truth about what happened.
Common questions
Frequently Asked Questions
What was the security vulnerability on WixSEO.co.uk?
The course_certificates table had a row-level security policy that allowed any anonymous user to insert a record directly into the database. This meant someone could forge a certificate claiming they had completed the Wix SEO course without actually doing so. A second table, professional_exam_passes, had fully open INSERT, UPDATE, and DELETE policies with the same risk.
Was anyone affected by this vulnerability?
No. The only forged record in the database was one I created myself during security testing to confirm the exploit worked. The professional exam table had zero rows, meaning nobody had used it at all. Both test records were deleted and the vulnerabilities were closed on the same day they were discovered.
How was the vulnerability discovered?
I discovered it during a routine audit of my database security policies on 23 August 2026. I was reviewing the row-level security configuration for all tables and noticed that the INSERT policy on course_certificates only validated name format and length, not whether the person had actually completed the course. I then tested it by inserting a forged record, which succeeded, confirming the vulnerability.
What is row-level security and why does it matter?
Row-level security (RLS) is a database feature that controls which rows a user can read, insert, update, or delete. When you use a service like Supabase, the database is accessible from the browser via an API key. RLS policies are the gatekeepers that prevent users from doing things they should not be allowed to do. If a policy is too permissive, anyone with the public API key can manipulate data directly.
How were the vulnerabilities fixed?
For course_certificates, the open INSERT policy was removed entirely. The table now has only a SELECT policy for public verification lookups. All certificate writes go through a server-side edge function that uses a privileged key and validates quiz completion before issuing. For professional_exam_passes, the INSERT, UPDATE, and DELETE policies were all removed, leaving only SELECT. The edge function handles all writes using the same server-side pattern.
Why are you disclosing this publicly?
Because transparency matters more than appearances. I advise clients on technical SEO and website security. If I find a problem on my own site and quietly fix it without saying anything, that is not the standard I want to set. Publishing this holds me accountable, gives other developers a concrete example to audit against, and demonstrates that finding and fixing vulnerabilities is normal maintenance, not a scandal.