Checking the upload
The API checks that the declared content type starts with image/ and uses Pillow to verify that the bytes can be opened as an image. Invalid image data gets an HTTP 400 response.
I made a tool that turns pet photos into transparent stickers for Catflakes. Making it work was one problem. Looking at it as a security engineer raises another: what happens when an API takes untrusted photos and asks a server to process them?
The app has a browser frontend and a Python backend that accepts uploads, reads image content, runs segmentation, and returns a cutout or finished PNG. It can also hand assets off to Catflakes. The inputs look harmless—pictures of cats—but they are still files supplied by people I don't control.
I wanted to look beyond “does it produce a sticker?” and ask which assumptions live at the boundary: what the file actually contains, what processing might cost, what the API exposes, and what GitHub checks before changes ship.
Scope: this is a source-based security case study of an application I built, not a penetration test or a claim that every proposed control is deployed. The application can be used publicly; its GitHub source links are private.
The API checks that the declared content type starts with image/ and uses Pillow to verify that the bytes can be opened as an image. Invalid image data gets an HTTP 400 response.
FastAPI parses typed form fields. Rendering options pass through a normalization step before they reach the image pipeline.
A combined processing endpoint segments a photo once; rendering from an existing cutout avoids repeating the heavier step for every border or crop adjustment.
GitHub Actions runs Python tests and a frontend build on pushes and pull requests. A separate Pages workflow sets explicit read, Pages-write, and OIDC-token permissions.
Tests cover successful image processing, PNG dimensions, candidate detection, Catflakes asset round-trips, and segmentation behavior. These are useful regression checks, but successful feature tests do not establish that hostile inputs are handled safely.
Today, _read_image_upload() reads the full file into memory before verifying it. I don't see explicit maximum request bytes or decoded-image pixel limits in this path. A valid but oversized or unusually complex image can still put pressure on CPU and memory. My next change would be bounded reads, decoded-dimension limits, and tests for the rejection behavior.
The API currently configures CORS with allow_origins=["*"] and allow_credentials=True. I would specify the frontend origins and determine whether credentials are needed at all. CORS is a browser policy, not API authentication, so it shouldn't be treated as the only access control.
The Catflakes bridge stores submitted packages in an in-process dictionary keyed by generated IDs. It's a convenient prototype, but there is no enforced expiry, per-object size ceiling, or authentication in that code path. I would add TTL cleanup, limits, and a clear rule for what the asset ID authorizes.
Current CI runs tests and a build. It does not currently show CodeQL, dependency scanning, secret scanning, DAST, or security-based merge blocking. I'd add SAST and dependency review incrementally, fix actionable findings, and only then describe them as operational controls.
These are observations from the current source and proposed hardening priorities. I have not claimed exploitation, measured impact, or completed remediation where the repository does not show it.
This is the direction I want for my software projects in general: security controls that are part of how the app is built and tested, not a security paragraph added after release.
The engineering work here is real: a browser app, an image-processing API, integration with another project, automated tests, and deployment workflows. The security work is a transparent review in progress. I'd rather show the actual boundary decisions and the work left to do than claim a “secure CI/CD pipeline” that I haven't built yet.
Related: what I learned doing vulnerability research in NYU AppSec →