DEV Community

Bedirhan Elibol
Bedirhan Elibol

Posted on

What a release check found in well-kept open-source apps

We keep a public benchmark of open-source projects that people copy from: SaaS starters, framework example apps, a few production products and some Android apps. They are well maintained and widely forked, and most of them have nothing seriously wrong. We use them to measure false alarms, so a person reads every high-severity finding on them before we count it as real.

Some of those findings held up. None of them is exotic. They are the kind of thing that gets copied into a new project on day one and is still there when it ships, so here they are with the fix for each. We left out project names because several are not fixed upstream yet, and the snippets show the shape of each issue rather than the exact upstream code.

A fallback secret that signs real tokens

An Express example app signs its JWTs with this:

const secret = process.env.JWT_SECRET || 'superSecret';
Enter fullscreen mode Exit fullscreen mode

Locally this is convenient. In production, if the variable goes missing because a deploy dropped it or someone renamed it, the app keeps running and signs tokens with a key anyone can read in the repo. Nothing errors, so nobody notices.

Fail at startup instead:

const secret = process.env.JWT_SECRET;
if (!secret) throw new Error('JWT_SECRET is not set');
Enter fullscreen mode Exit fullscreen mode

A storage policy that only checks the bucket

A test migration in a database client library has a storage policy shaped like this:

create policy "avatars" on storage.objects
  for all using (bucket_id = 'avatars');
Enter fullscreen mode Exit fullscreen mode

for all covers insert, update and delete, and the condition never looks at who owns the file, so any signed-in user can overwrite or delete anyone's avatar. The same project has a using (true) read policy that makes every row of a table public. In an example that may be on purpose, but example migrations are exactly what people paste into real projects.

Scope write policies to the owner:

create policy "owners update own files" on storage.objects
  for update using (bucket_id = 'avatars' and (select auth.uid()) = owner_id::uuid);
Enter fullscreen mode Exit fullscreen mode

A SECURITY DEFINER function without a search path

A popular subscription starter creates a trigger function that runs with its owner's privileges and no fixed search_path:

create function public.handle_new_user() returns trigger
language plpgsql security definer as $$ ... $$;
Enter fullscreen mode Exit fullscreen mode

The function then resolves table names through whatever schemas the calling session puts first. If a user can create objects in one of those schemas, they can shadow a table the function writes to and run code as the owner. Pin the path and schema-qualify the tables:

create function public.handle_new_user() returns trigger
language plpgsql security definer set search_path = '' as $$ ... $$;
Enter fullscreen mode Exit fullscreen mode

The same starter pins Next.js 14.2.3. That version is affected by CVE-2025-29927, where a request header lets a caller skip middleware, including any auth check that lives there. The fix is an upgrade, plus a dependency check in CI so the next advisory shows up in a pull request.

Containers that run as root and a database on every interface

Several Dockerfiles in the set have no USER line, so the app runs as root inside the container. A compose file publishes Postgres like this:

ports:
  - "5432:5432"
Enter fullscreen mode Exit fullscreen mode

That binds to all interfaces. On a laptop it hardly matters. On a VPS without a firewall in front, the database is on the internet. Use "127.0.0.1:5432:5432" unless something outside the host needs it, and give the image a non-root user:

RUN useradd --create-home app
USER app
Enter fullscreen mode Exit fullscreen mode

CI actions pinned to a tag

Workflows reference third-party actions as some-org/some-action@v3. Whoever controls that repo can move the tag to different code, and the job often holds a token with write access. Pin to the full commit SHA and keep the tag in a comment, so the code that runs is the code you reviewed:

- uses: some-org/some-action@<full-commit-sha> # v3
Enter fullscreen mode Exit fullscreen mode

Android: cleartext traffic, exported components and a feed parser

An open-source Android podcast app sets android:usesCleartextTraffic="true" so feeds and media can load over plain HTTP. For a podcast player that is a real trade-off, since some feeds are still HTTP only, but the flag applies to every request the app makes. A network security config can allow cleartext for the hosts that need it and keep everything else on HTTPS.

The same app exports a broadcast receiver and a widget that accept its own custom actions, so any other installed app can send them. If only the system needs to reach a component, a permission on it, or a separate unexported component for the app's internal actions, closes that.

It also parses remote feeds with a default SAXParserFactory and no DTD or external-entity settings, on XML that comes straight from the internet. Whether that is exploitable depends on the parser underneath, which is a good reason to set those features explicitly and test them on the Android versions you support.

A database upgrade step builds UPDATE ... WHERE file_url='...' by concatenating a file path into the string. A path with a quote in it breaks the query. That is a bug before it is a security issue, and a bound parameter fixes both.

A migration that locks a busy table

One migration creates an index on an existing table without CONCURRENTLY. On Postgres, a plain CREATE INDEX blocks writes to the table until the build finishes. On a small table nobody notices. On a large one, the deploy that runs it stops writes for as long as the index takes.

create index concurrently if not exists idx_orders_user on orders (user_id);
Enter fullscreen mode Exit fullscreen mode

CONCURRENTLY can't run inside a transaction, so some migration tools need it in a migration of its own.

Checking your own project

None of this needs a pentest. Each issue is visible in code or config, and each fix is a few lines. The hard part is noticing before release, when nobody is reading the Dockerfile or the old migrations anymore.

We build Zelsis to run that check on every pull request, across code, dependencies, migrations, containers and CI config, in most languages. Public repos get a free scan at zelsis.com. If you maintain one of the projects above and want the details, write to contact@zelsis.com and we'll send what we found.

Top comments (0)