July 30, 2026
Cybersecurity Case Study of TrustEats Website
How I Secured a Product Authentication Platform from Scratch: A Security Engineer’s Walkthrough of TrustEats
By Chikezie Ilodigwe
14 min read
A Build Season case study on threat modelling, API security reviews, live vulnerability discovery, and what it actually looks like to secure a safety-critical web application.
Background
For our Build Season project, my team built TrustEats -a web-based product authentication platform designed for the Nigerian market. The idea is simple: consumers scan a QR code on a product's packaging, and the platform instantly tells them whether the product is genuine, suspicious, or fake. Manufacturers register their products, generate unique QR codes per production batch, and the system keeps a verified database of every code ever issued.
My role on the team was Cybersecurity Engineer. But "cybersecurity engineer" on a team building a product from scratch doesn't mean sitting on the sidelines running scans. It means being in the weeds from day one, defining what the architecture should look like, writing threat models before a single line of code exists, reviewing API designs, catching vulnerabilities in code that other people wrote, and sometimes (when the backend developer went missing for two days) writing the backend yourself just to keep the project alive (for some of us with Dev experience though).
This is the story of how I approached security on TrustEats, what I found, what I fixed, and what I learned.
Why This Project Is Security-Sensitive
Most web applications have a relatively standard threat profile: protect user data, prevent account takeovers, stop SQL injection. TrustEats has all of that plus one additional requirement that changes the entire security calculus.
The core output of TrustEats is a trust signal. When a consumer scans a QR code and sees "Genuine," they might feed that product to their infant. When they see "Fake," they put it back on the shelf. That trust signal has to be tamper-proof. If a bad actor -say, a manufacturer of counterfeit baby formula, can manipulate that signal to return "Genuine" for their fake product, the security failure is not a data breach. It is a public health event.
That single insight shaped every security decision I made on this project. Everything else was secondary to one question: can the verification result be manipulated? And my job was to make sure the answer was no, permanently, by design.
The Methodology
I didn't work from a single checklist. I layered multiple frameworks together depending on what I was looking at:
STRIDE for threat modelling, I used this to systematically ask "what could go wrong" across every component. STRIDE stands for Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, and Elevation of Privilege. For a product authentication platform, Tampering was the dominant category because the entire product is a trust signal.
OWASP Top 10 (2021) as a validation layer, after designing controls, I mapped them back to OWASP to confirm I hadn't missed any well-known vulnerability class. This is particularly useful for catching things that feel obvious in retrospect but are easy to overlook when you're deep in a specific threat model.
Custom risk register — I created a risk register with 20 entries, each rated by Likelihood (1–5) and Impact (1–5). Risk Score = L × I. Anything 15 or above was Critical, 10–14 was High. This gave the team something concrete to prioritize against.
Adversarial testing — toward the end of the build, I actively tried to break the system. Not with automated scanners, but manually, thinking like each threat actor I'd identified. What would a counterfeit manufacturer actually try? What would an automated bot do? What edge cases would a developer not think to test?
Across the build season I produced four security documents: a Threat Model, a Database Design and Security Review, an API Security Review, and a Final Security Assessment. Each one fed into the next.
Document 1: The Threat Model
I wrote the threat model before the team had written any significant code. This was intentional. Security is dramatically cheaper when it shapes design rather than being retrofitted into something that already exists.
Defining the Attack Surfaces
I identified 15 attack surfaces across the platform. The top ones were:
The verification endpoint — POST /api/v1/verify and GET /api/v1/verify/:code. This is a public endpoint, no authentication required, and it's the highest-traffic surface in the entire system. Any consumer with a phone can hit it. This makes it the primary target for automated abuse, code enumeration attacks, and NoSQL injection.
The verification code generation algorithm — whoever controls how codes are generated controls authenticity. If codes are predictable (sequential integers, short alphanumeric strings), a counterfeiter can pre-generate valid-looking codes for fake products.
The verification result itself — if there's a field in the database called verificationStatus that a manufacturer can update through their dashboard, the entire product is broken. The architecture has to make this impossible by design.
The manufacturer authentication layer — a compromised manufacturer account gives an attacker the ability to register fake products, generate real-looking QR codes for counterfeit goods, and suppress fake scan analytics.
The counterfeit report submission endpoint — supports image uploads, accessible without authentication. Every file upload is an attack surface.
Defining the Threat Actors
I profiled 8 threat actors. The two most important ones were:
TA-01: The Counterfeit Manufacturer — This is the primary adversary for TrustEats specifically. They are not a generic hacker trying to steal data. They have a financial motive to make their fake products appear genuine on our platform. They know the product codes on their fake packaging. They are technically capable enough to hire help if needed. This threat actor is what keeps the verification result integrity requirement at the top of the priority list.
TA-02: The Rogue Manufacturer — A legitimate, approved manufacturer who decides to manipulate their own product's verification results. Maybe a batch gets contaminated and instead of issuing a recall, they try to suppress the data. This is an insider threat with authenticated access, which makes it more dangerous than an external attacker in some ways.
The others ranged from automated bots (TA-03) to supply chain attackers (TA-06) to nation-state actors (TA-08, low likelihood at MVP stage but worth documenting).
The STRIDE Analysis
I ran STRIDE across all major components and produced 17 specific threat entries. A few that stood out:
On Tampering: "Manufacturer modifies their own product's verification status from Fake to Genuine in the DB." This was rated Critical impact because the consumer harm is direct and physical. My mitigation recommendation was architectural: the verification result must be computed server-side from multiple independent data sources at query time. There must be no API endpoint that accepts a verification status as input.
On Information Disclosure: "IDOR on the transaction endpoint — manufacturer A accesses manufacturer B's scan analytics." This is commercially sensitive data and a straightforward IDOR vulnerability if queries aren't properly scoped. Mitigation: always derive manufacturerId from the JWT payload, never from the client request.
On Denial of Service: "Bot floods the verification endpoint." Mitigation: aggressive rate limiting — 10 requests per IP per minute on the scan endpoint. This endpoint is public and must stay performant for real consumers.
The Risk Register
The top risks by score:
Risk Score Rating Verification code enumeration (SR-08) 16 CRITICAL DoS on verification endpoint (SR-09) 16 CRITICAL Phishing site impersonating TrustEats (SR-16) 16 CRITICAL Manufacturer tampers with verification result (SR-01) 15 CRITICAL NoSQL injection on verification endpoint (SR-03) 15 CRITICAL
Document 2: The Database Design and Security Review
After the threat model, I worked with the backend developer to define the MongoDB schema and then reviewed it for security. This is where the architectural defense of the verification result gets implemented.
The Core Decision: Computed Result, Not Stored Result
The most important database security decision in TrustEats is that there is no verificationStatus field. The result is computed at query time by checking six independent conditions in sequence:
- Does the code exist in the database?
- Is it active (
isActive: true)? - Is the manufacturer approved?
- Has the batch been recalled?
- Is the batch expired or flagged?
- Has the code been scanned more times than expected?
The result comes from the combination of these conditions. To manipulate it, an attacker would need to simultaneously compromise the verification codes collection, the batches collection, and the manufacturers collection. There is no single field to tamper with.
This design also means that when an admin recalls a batch, every code in that batch immediately starts returning "Fake" — no per-code update needed. It's instantaneous and can't be partially applied.
Write Permission Matrix
I documented who can write to each collection. The most important row in that matrix:
Collection Consumer Manufacturer Admin System verificationcodes — create NO NO NO YES only verificationcodes — isActive NO NO YES NO
The manufacturer role has zero write access to the verification codes collection. This is the database-level enforcement of the architectural guarantee. No manufacturer-facing API route creates or modifies verification codes. Code generation is a system-only operation triggered by batch creation.
The Audit Log
I insisted on an append-only audit log collection. Every significant state change — manufacturer approval, product creation, batch recall, code deactivation, report review — creates an immutable record with the previous value, new value, who did it, when, and from what IP.
The audit log has no delete or update route for any role, including Admin. This was a conscious design decision. If something goes wrong in production — a contaminated batch that someone tried to quietly suppress, a rogue admin modifying product data — the audit trail has to be inviolable.
Document 3: The API Security Review
The backend developer submitted an API design document midway through the build. I reviewed it against the threat model and found 18 security findings — 5 Critical, 6 High, 4 Medium, 3 Informational.
Here are the ones that mattered most:
AF-01 (Critical): JWT in the Response Body
The API doc specified returning the JWT token in the JSON response body:
{ "token": "eyJhbGciOi..." }{ "token": "eyJhbGciOi..." }This is a textbook XSS vulnerability. Any JavaScript running on the page can read the response body and steal the token. For a platform that handles product safety decisions, this is unacceptable.
The fix: deliver tokens exclusively via HttpOnly cookies. JavaScript cannot read HttpOnly cookies. The response body returns only non-sensitive user data.
This is one of those findings where the developer's response was "but everyone does it that way." That's true. Bearer tokens in response bodies are common. They're also why a lot of applications are vulnerable to session hijacking via XSS.
AF-02 (Critical): No Batch Model
The API doc had a flat product-to-codes structure. Codes attached directly to products. No batch entity.
The problem: without batches, you can't recall a specific production run. If a product has five years of production runs and one of them is contaminated, you have to either recall the entire product (affecting all batches) or do nothing. You also can't track manufacturing date and expiry date per production run, which means you can't automatically detect expired codes.
This was a fundamental architectural gap. The fix was implementing the batch model with its lifecycle states (active, expired, recalled, flagged) and making the verification result dependent on batch status.
AF-04 (Critical): No Input Validation Before DB Query
The verification endpoint accepted any string as input and queried the database with it. This means:
- NoSQL injection payloads hit MongoDB before being rejected
- Sequential guessing strings generate database load
- An attacker can probe the query behaviour based on response timing
The fix: validate UUID v4 format with a regex before any database operation. If the input doesn't match the expected format, return the standard "Fake" result immediately. No DB query. No timing difference. No information leakage about the expected format.
AF-05 (Critical): No Audit Logging
No audit trail on any state-changing operation. Products could be created, batches could be recalled, codes could be revoked — all without any record of who did what.
The fix was implementing the AuditLog collection I'd specified in the database review.
The Backend Developer Problem
About two days into the build, I realized the backend developer wasn't making progress. He said he was "on transit." No code, no API design beyond the initial document, no response to messages.
This created a decision point: do I wait for him and write security documentation against something that doesn't exist, or do I step in?
I have a MERN stack background. I know Node, Express, MongoDB, and TypeScript. So I started off the backend myself.
This was the right call for the project but it taught me something important about being a cybersecurity engineer on a cross-functional team: your job is not just to audit other people's work. Sometimes the most valuable security contribution you can make is ensuring there's a working, secure implementation to demonstrate. An empty codebase with perfect documentation is worth nothing at demo time.
Building the backend also gave me something I wouldn't have had otherwise: total visibility into the implementation. I could guarantee that every security recommendation I'd made in the threat model and API review was actually implemented, because I implemented it myself.
The Security Testing Phase
With the backend deployed on Render, I ran 35 structured and adversarial tests. I'm going to walk through the ones that were interesting.
The Three Vulnerabilities I Found
During adversarial testing, I found three real vulnerabilities that weren't caught during code review:
Vulnerability 1: NoSQL Injection in the Login Body
I sent this as the login request body:
{ "email": { "$gt": "" }, "password": "anything" }{ "email": { "$gt": "" }, "password": "anything" }The system returned HTTP 500. Not 401, not 400–500. This meant the payload made it through to Mongoose and caused an internal error. It didn't actually grant access, but a 500 response from an auth endpoint is bad for two reasons: it tells an attacker that their injection is reaching the database, and the error message in development mode was exposing internal details.
The fix was a type guard at the top of the login controller:
if (typeof email !== 'string' || typeof password !== 'string') {
res.status(400).json({ success: false, error: 'Invalid input' });
return;
}if (typeof email !== 'string' || typeof password !== 'string') {
res.status(400).json({ success: false, error: 'Invalid input' });
return;
}Vulnerability 2: NoSQL Injection in Query Parameters
I sent this request:
GET /api/v1/transactions?category[$ne]=FoodGET /api/v1/transactions?category[$ne]=FoodThis returned 200 with 48 transactions. My test user had 60 transactions total. The $ne (not equal) MongoDB operator had executed — the query returned everything except the Food category.
This means an attacker could use MongoDB query operators through URL query parameters to manipulate how data is filtered. On a different endpoint, this could expose data belonging to other users.
The fix was a global middleware that runs on every request before any route handler:
const stripOperators = (obj: Record<string, unknown>): void => {
for (const key of Object.keys(obj)) {
if (key.startsWith('$') || key.includes('.')) {
delete obj[key];
}
// recurse into nested objects
}
};const stripOperators = (obj: Record<string, unknown>): void => {
for (const key of Object.keys(obj)) {
if (key.startsWith('$') || key.includes('.')) {
delete obj[key];
}
// recurse into nested objects
}
};After the fix, the same request returned all 63 transactions (60 seed + 3 from testing) — the operator was stripped and the filter was ignored entirely.
Vulnerability 3: XSS in Transfer Description Causing HTTP 500
I sent a transfer request with this description:
<img src=x onerror=alert(1)><img src=x onerror=alert(1)>The sanitize-html library stripped the HTML, leaving an empty string. But the description field in the Transaction schema had required: true, so Mongoose rejected the empty string and the controller returned HTTP 500 with a validation error message.
Two problems in one: the 500 exposed internal error details, and the empty sanitised value wasn't handled gracefully.
The fix was a description fallback in the transfer controller:
const safeDescription = description?.trim() || 'Transfer';const safeDescription = description?.trim() || 'Transfer';All three vulnerabilities were fixed in the same testing session. Running the same tests after the fixes: all three returned the correct responses.
The Tests That Passed Clean
The most satisfying tests to run were the ones that validated the core security architecture was sound:
Verification result integrity — I recalled a batch via the admin endpoint, then immediately scanned one of the batch's codes. It returned "Fake" instantly. No cache flush needed, no per-code update, no delay. The architectural decision to compute the result at query time paid off exactly as designed.
IDOR on manufacturer data — I logged in as Manufacturer A and tried to access Manufacturer B's product ID. Got 404. Tried to create a batch using Manufacturer B's product ID. Got 404. Tried to view Manufacturer B's analytics. Got only Manufacturer A's data. Every manufacturer query was correctly scoped to the authenticated user's manufacturer ID from the JWT.
Auth bypass — Tried to access the dashboard, analytics, and product endpoints without any authentication cookie. Got 401 on every one.
What I Actually Learned
Security is an architecture problem before it's an implementation problem
The most important security decisions on TrustEats were made in the design phase, not the testing phase. The decision to compute the verification result rather than store it, to use UUID v4 for codes, to make the audit log append-only — these were architectural choices. By the time code was being written, the security properties were already either baked in or missing.
This is the core value of threat modelling early. You're not just writing documentation. You're making design decisions under the lens of "how would someone break this."
The STRIDE framework is genuinely useful, not just box-ticking
Before this project, I'd read about STRIDE but hadn't applied it to something real. What surprised me is how naturally it reveals gaps. When you ask "what Tampering attacks exist on this component," you naturally ask "who has write access to this data?" and "is there any way to change this value without going through the intended flow?" Those questions surface design problems that code review alone wouldn't catch.
Threat actors change your priorities
Generic security advice says "sanitize inputs, use HTTPS, hash passwords." All true. But when you profile your specific threat actors, you find things that generic advice doesn't cover.
For TrustEats, the counterfeit manufacturer (TA-01) threat actor forced me to think about the manufacturer dashboard specifically as an attack surface. Most platforms trust their authenticated users more than their anonymous users. TrustEats can't fully trust manufacturers — they have a financial incentive to manipulate the system. That insight drove the design of the write permission matrix and the verification result computation.
Finding vulnerabilities during testing is not a failure
When I found the three vulnerabilities during adversarial testing, my initial reaction was mild frustration. I'd reviewed the code. Why didn't I catch these?
Then I thought about it differently. The NoSQL injection in query params was subtle because Express parses bracket notation in URLs into nested objects — ?category[$ne]=Food becomes { category: { $ne: 'Food' } } in req.query. That's not immediately obvious. Finding it during testing is exactly what testing is for.
The lesson: code review and testing catch different things. You need both.
Working across a team with varying security awareness is a skill
The backend developer's API design had 18 security findings, including some fundamental ones like returning JWTs in the response body. This isn't a criticism of him specifically — these are common patterns that feel reasonable until you understand why they're problematic.
The challenge as a security engineer is communicating findings in a way that's actionable, not condescending. Writing a findings document that says "this is wrong and here's why" is easy. Writing one that explains the threat actor, the exploitation scenario, and the concrete fix — in language a developer can understand and act on — is harder and more important.
The Challenges
The backend developer going offline — Already covered this, but it's worth naming as a challenge because it required a real decision. In a corporate setting, escalating to a project manager and waiting is the safe move. In a time-boxed build season with a demo in days, waiting wasn't an option. The lesson: on a small team, your role descriptions are guidelines, not boundaries.
Balancing security with demo deadline — Some security items I identified as important couldn't be implemented in the build season timeline. Redis caching with cache invalidation on batch recall, for example. The security risk of not having it (a stale "Genuine" result for up to 5 minutes after a batch recall) is real but acceptable at demo scale with zero real users. Knowing which risks to accept and document versus which to block on is a judgment call that gets easier with experience.
Resend free tier limitations — We implemented OTP-based email verification, integrated Resend, wrote the email templates, wired everything up. Then discovered that Resend's free tier only sends to verified email addresses. So during testing, OTPs only arrived in the inbox of the email address used to sign up for Resend. For the demo, this means testers have to use specific email addresses. Not a security bug, but a frustrating operational limitation that cost debugging time.
What I Would Do Differently
If I were starting this project again from scratch:
Write a data flow diagram before the threat model — I jumped straight to attack surfaces without first mapping out how data moves through the system end-to-end. A data flow diagram would have made the threat model more systematic and caught a few things I had to circle back to.
Get the backend developer to review my threat model earlier — I wrote the threat model largely independently and then presented it to the team. If I'd run a working session with the backend developer to walk through the threat model together, he might have caught implementation gaps earlier, and I would have learned about his planned architecture before it was built.
Implement Redis and cache invalidation from the start — Deferring this to Sprint 2 was the right call given time, but the security property of "recalled batch immediately causes Fake on all cached responses" is important enough that I'd push to include it in the initial build if I had more time.
Final Thoughts
Securing TrustEats was genuinely interesting work because the stakes felt real. Counterfeit food is a real problem. The threat actors aren't hypothetical — counterfeit product manufacturers exist and they have real financial incentives to compromise a platform like this.
What I took away most from this build season is that security engineering at its best is not about finding bugs. It's about understanding the system you're protecting well enough to know what failure would actually look like, and then making it structurally difficult for that failure to occur.
The verification result in TrustEats can't be tampered with because there's no field to tamper with. The audit log can't be deleted because there's no delete route. The OTPs expire in 10 minutes because anything longer is unnecessary risk. These aren't clever hacks. They're design decisions made with a clear understanding of who would try to break the system and how.
That's what security engineering is.
This case study was written as part of the Learnable 2015/2026 program Build Season . TrustEats is a product authentication platform built to protect Nigerian consumers from counterfeit food products. The backend is live at Render and the full security documentation — Threat Model, Database Security Review, API Security Review, and Final Assessment — is available in the project repository. Thank you.