Cybersecurity case study of MedusaLink

In this article, I am going to present and document a cybersecurity case study that I carried out on MedusaLink.

1. Table of Contents

  1. Table of Contents
  2. Introduction
  3. Phase 1: Threat Modeling
  4. Phase 2: Discovery
  5. Phase 3: Remediation
  6. Phase 4: Automation
  7. Outro

2. Introduction

Goals

The aim of this project was two-fold.

The first goal was to assess and improve the defensive capabilities of MedusaLink. While I always had an eye on the security aspect during implementation, with 30+ users and having been in production for a year, I decided this deserves a bit of dedicated attention.

The second goal was for me to gain practical experience with application security. I am currently aiming for a switch into cybersecurity, and this project was just the right opportunity for me to get a bit more hands-on with a real-world case, without having to deal with an overwhelmingly large project right away.

What is MedusaLink?

MedusaLink is a marketing platform unifying the principles of traditional cashback programs, coupon codes and word-of-mouth marketing. Through MedusaLink, online shoppers can access discounts, promoters can earn commissions, and merchants can attract new customers.

The project has been in development since September 2024 by a two-man team: my former colleague who came up with the idea and handles business development, and myself who is responsible for everything technical from implementation to operation. MedusaLink first went live in production as an MVP in July 2025. As of this article in August 2026, it is still running and has 30+ users, but is no longer being actively developed.

Scope

As a first step, I threat modelled the application in order to get a good grasp of what I'm dealing with. Then I drafted and executed SAST/DAST test cases for each component of the system. Next, I analyzed the findings of both previous steps, decided on courses of action, and addressed the discovered weaknesses wherever applicable. Finally, I integrated basic security testing into my CI/CD pipeline as a safe guard for any additional features going forward. I continually documented my steps the entire way as a reference to myself and as an aid for writing this article.

Disclaimer

Naturally, writing a publically accessible article about the security measures of a project can be a bit delicate and reveal details you may not want malicious actors to know about. Therefore I tried to omit specific details where possible without losing substance. The emphasis of the article is more the process rather than the specifics.

3. Phase 1: Threat Modeling

As I only had surface level familiarity with threat modeling, I first did some research and found the Threat Modeling Manifesto which defines the process as 4 questions that need to be answered.

  1. What are we working on?
  2. What can go wrong?
  3. What are we going to do about it?
  4. Did we do a good enough job?
For each step, I referred to the OWASP Threat Modeling Cheatsheet. The cheatsheet recommended the STRIDE methodology which I'd already heard about as well as OWASP Threat Dragon as a tool, so I decided to use these.

What are we working on? - MedusaLink architecture

MedusaLink is very simple in terms of architecture: we are using a JSON-based RESTful API with a relational database, and a frontend UI in front of it, in this case a Typescript-based Nuxt application. Additionally, parts of the API are not accessed through the frontend and are meant for external, customer systems to integrate with as well as various admin-level operations.

threat model

What could go wrong? What are we going to do about it?

Identifying possible threats was a bit more challenging than I anticipated. The main reason was that I was missing a structured approach so I couldn't guarantee I didn't miss anything. Even the cheatsheet only mentions approaches like brainstorming, or at a higher level using something like MITRE ATT&CK. I also found the DFD and STRIDE itself a bit awkward to work with, but let's not get into detail about that here, I will talk about it a bit at the end.

For the sake of not letting the project take forever, I decided to simply evaluate components one-by-one, try to come up with possible vulnerabilities myself as well as reference the OWASP Top 10, and map these to components in the DFD and to STRIDE categories. Some items that didn't fit in there neatly were documented in a separate markdown file.

Below is a summary of my findings. In order to not expose too much, I decided to only include general summaries of the areas I decided to pay special attention to. In short, many of these categories were already handled, but some needed further mitigations. I decided what actions I would take on each item as well, but left implementation for later.

Did we do a good enough job?

While reviewing and validating the threat model is not 100% possible when working alone as there simply aren't multiple eyes to spot gaps, I did my best to ensure a few basic criteria are met by referring once again to the cheatsheet.

Phase 2: Discovery

After completing threat modeling, my next goal was to run some SAST/DAST tests. The two components to be tested were the frontend and backend.

SAST

Initially, I was eyeing SemGrep for static analysis, however I found out that while their core is free and open source, the project has now transitioned to a paid proprietary model. Luckily, OpenGrep exists as a FOSS alternative under the LGPL license, and just like SemGrep, supports both Go and Typescript.

The fact that OpenGrep provides a simple CLI also appealed to me: all we need to do is run opengrep scan --sarif -o OUTPUT_FILE in the root directory of the backend project. This simplicity also meant that this would be very easy to integrate into my CI/CD pipeline later.

For the backend codebase, 5 potential problems were found, all of them only warning level:

For the frontend codebase, additional --include and --exclude patterns needed to be specified because otherwise opengrep would not have looked at .vue files, meanwhile we don't want to scan the infamously enormous node_modules. Likewise no major findings were reported by opengrep.

DAST

For dynamic testing, I spent a bit more time than I would have liked picking a tool. I was familiar with Burp, however it felt like too large and complicated of a tool for what I needed, not to mention some of the tools that would have been most useful to me are not available in the free version. An alternative I looked at was Zaproxy, but I likewise found it too large for my needs and it also had performance problems where running large scans would eat up so much RAM that it would freeze my entire system.

After some digging around, I found vulnapi. It is a more minimal alternative that does exactly what I need and little more: it's fast (being a mere CLI program), lightweight, open source under the MIT license, and to top it off, built in Go which I always like seeing.

However, vulnapi is specifically meant for testing APIs only, for the frontend, I would need a different tool that can also attack HTML pages, including JS-heavy ones. I found Wapiti, also a command-line tool with many options, including the ability to explore frontend applications, just as I needed.

Scanning the backend API

vulnapi supports OpenAPI specification based scanning. Luckily, this was already present for MedusaLink, however due to the limitations of the library I used, only in version 2.0. vulnapi doesn't state official support for this and it also didn't work when I simply tried, therefore I first had to convert the spec to version 3.1 via the official Swagger Editor.

An additional preparation step was to implement a backup-restore functionality for the test database, as the scan would pollute the database with records that I didn't want to keep around after the testing. I implemented this via two simple shell scripts.

As the test environment is running on a small VPS, I also decided to rate limit all my testing to 10 requests per second. This is probably far below what the server could actually handle, but it was fast enough for my purpose.

As the application has a few different types of users and different authentication mechanisms, I identified the following scopes that each needed to be tested:

The initial discovery scan looking for commonly exposed paths only discovered our healthcheck endpoint and the fact that we are using a Traefik reverse proxy on the test environment. This doesn't mean a whole lot since this is only a list-based discovery and not a full crawl, but at least we know we are not accidentally exposing anything low-hanging.

With all this considered, my scans would look something like the following. For cookie-based sessions, the session cookie had to be passed manually and the OpenAPI spec needed to be edited to exclude the logout endpoint, as a call to that would mess with the later results.

vulnapi scan openapi --rate 10/s swagger.yml --security-schemes [API key key-value pair]

The only findings were about a few missing security headers in the HTTP responses, namely:

Scanning the frontend application

wapiti already includes most tools (which it calls "modules") that we need by default, with a few additional ones that made sense to enable via the -m argument. Wapiti can also use a headless Firefox browser, which is something we do want when working with a JS-heavy SPA like ours. Limiting the request rate yet again was necessary, albeit Wapiti doesn't provide a direct req/s adjustment, but an "intensity" switch instead as well as the ability to limit the number of parellel requests running at a time. I ran my scan as follows:

wapiti --headless=hidden -S=polite --tasks=2 -m=common,csrf,buster,methods -u https://test.medusalink.com

The scan only discovered the same missing headers as in the case of the backend, and in this case, those headers were much more important to include as well.

Dependency scanning

Supply chain vulnerabilities have unfortunately been an area I'd mostly ignored up to this point other than basic sanity checks when picking my dependencies and occasionally updating them when I remembered. This is problematic since this class of vulnerabilities is third placed in the 2025 OWASP Top 10. After enabling Dependabot alerts, 150 vulnerabilities were discovered, most of them in npm packages, some of them high or even critical severity. I marked these as to-be-resolved in the next stage as well.

Phase 3: Remediation

Once I was done identifying potential vulnerabilities, it was time to address them. I first looked at the issues find by Dependabot as these were the largest in number, most severe, and meanwhile also potentially fixable by just upgrading everything.

Next, I fixed the findings of my SAST/DAST tests as these were trivial to implement and test. As for the higher level areas identified during threat modeling, many of these were already well covered, however some are more interesting to look at.

Supply chain

As mentioned, the initial Dependabot scan found 150 vulnerabilities in total. Fortunately, all of them were fixed by simply upgrading every dependency to their newest version, albeit this introduced a few bugs and came with a few painful migration processes. By letting Dependabot auto-generate patches and regularly taking a look at them, this workload will be more manageable in the future.

Rate limiting

Next to the usual overall and per-ip limits, I also opted to include a per-seller rate limit, the rationale being to mitigate a case where a business customer's system gets taken over by an attacker who then tries to use this foothold to DoS MedusaLink itself. The per-ip rate limit would likely defend us against this anyway, but by also limiting the requests per seller, we also protect them. A sudden burst of incoming transactions would be highly unusual in the first place, so this way we can also limit the number of fraudulent transactions that will make it into MedusaLink (and later need to be cleaned up) in case of a compromised customer system.

Another decision was whether to handle rate-limiting on the infra level or the application level. Our hosting provider, Render provides DDoS protection via Cloudflare, however Render states that likely not everything will be filtered out, since it can be difficult to tell apart legitimate and malicious layer 7 traffic. Because of this, and the per-seller rate limit, application level handling is still necessary.

The overall and per-seller limits were trivial to implement using the go-chi/httprate library, while the per-ip limit was a bit more challenging due to the differing ways in which proxies, providers etc pass the real ip of the client. Render uses Cloudflare, however unlike Cloudflare it places the real ip into the first slot of the x-forwarded-for header. It does however pass CF-Ray down to us, so we can use this header to detect that we're on Render. Meanwhile on the test environment, Traefik places the real ip into the last slot of x-forwarded-for.

Logging

After reviewing all the logs the application produces, I found that they were not detailed enough and some operations were not documented beyond simple request logging. I added logs for all important operations, paying special attention to admin level ones. I also re-worded and added detail to already existing logs.

Another important way in which logs can help mitigate repudiation issues is retention. Render's cheapest tier offers only 7 days of log retention. While this is not ideal, the alternatives would be upgrading to a higher tier which would be significantly more costly, or streaming logs to a third party provider, however the options I looked at are also not much better in terms of pricing and retention. Therefore I decided to leave this as is.

Finally, another way to make logs more reliable is by cryptographically ensuring their integrity. This would be complicated (and not advisable) to handle inside the application, while Render seems to provide no easy (or cheap) way to achieve this. Therefore I also decided to leave this out for now.

Phase 4: Automation

I was originally planning this last phase as a ground to get to know DevSecOps practices a little better, however in practice this section ended up being shorter than I expected.

As mentioned, I enabled Dependabot alerts for the project as well as automated pull requests for security patches and version bumps. Additionally, the SAST scan via OpenGrep was easy to integrate due to a GitHub Action already being available for this. Conveniently, the sarif reports can also be uploaded to the Security tab of the repository, which is the same place where Dependabot alerts live.

DAST on the other hand is a tougher nut to crack. I decided not to automate this for now as it would require quite a bit more work on the infrastructure front. For now, running these scans manually pre-merge against the test environment will do.

Closing words

My goal when starting this project was to dip my toes into the field of Application Security, as well as strengthen the defensive capabilities of MedusaLink. I am happy with how the project went and I would definitely say that I've succeeded in both goals.

My three most important take aways are the following:

Overall, I've enjoyed this project a lot and it has been exactly the learning opportunity I'd intended it to be. It's also made me all the more excited about entering the world of cybersecurity and made me realize just how much more there is for me to discover, which I'm looking forward to.