<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bedirhan Elibol</title>
    <description>The latest articles on DEV Community by Bedirhan Elibol (@bedirhandev).</description>
    <link>https://dev.to/bedirhandev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4185338%2F7ae60fba-27d0-4be8-8638-20ae9c54b512.jpg</url>
      <title>DEV Community: Bedirhan Elibol</title>
      <link>https://dev.to/bedirhandev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bedirhandev"/>
    <language>en</language>
    <item>
      <title>What a release check found in well-kept open-source apps</title>
      <dc:creator>Bedirhan Elibol</dc:creator>
      <pubDate>Wed, 14 Oct 2026 18:53:11 +0000</pubDate>
      <link>https://dev.to/bedirhandev/what-a-release-check-found-in-well-kept-open-source-apps-pm6</link>
      <guid>https://dev.to/bedirhandev/what-a-release-check-found-in-well-kept-open-source-apps-pm6</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fallback secret that signs real tokens
&lt;/h2&gt;

&lt;p&gt;An Express example app signs its JWTs with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;superSecret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Fail at startup instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;JWT_SECRET is not set&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A storage policy that only checks the bucket
&lt;/h2&gt;

&lt;p&gt;A test migration in a database client library has a storage policy shaped like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"avatars"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'avatars'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;for all&lt;/code&gt; 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 &lt;code&gt;using (true)&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Scope write policies to the owner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"owners update own files"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'avatars'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;owner_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A SECURITY DEFINER function without a search path
&lt;/h2&gt;

&lt;p&gt;A popular subscription starter creates a trigger function that runs with its owner's privileges and no fixed &lt;code&gt;search_path&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_new_user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt;
&lt;span class="k"&gt;language&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt; &lt;span class="k"&gt;definer&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_new_user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt;
&lt;span class="k"&gt;language&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt; &lt;span class="k"&gt;definer&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Containers that run as root and a database on every interface
&lt;/h2&gt;

&lt;p&gt;Several Dockerfiles in the set have no &lt;code&gt;USER&lt;/code&gt; line, so the app runs as root inside the container. A compose file publishes Postgres like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;"127.0.0.1:5432:5432"&lt;/code&gt; unless something outside the host needs it, and give the image a non-root user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;--create-home&lt;/span&gt; app
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CI actions pinned to a tag
&lt;/h2&gt;

&lt;p&gt;Workflows reference third-party actions as &lt;code&gt;some-org/some-action@v3&lt;/code&gt;. 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;some-org/some-action@&amp;lt;full-commit-sha&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;# v3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Android: cleartext traffic, exported components and a feed parser
&lt;/h2&gt;

&lt;p&gt;An open-source Android podcast app sets &lt;code&gt;android:usesCleartextTraffic="true"&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;It also parses remote feeds with a default &lt;code&gt;SAXParserFactory&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;A database upgrade step builds &lt;code&gt;UPDATE ... WHERE file_url='...'&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  A migration that locks a busy table
&lt;/h2&gt;

&lt;p&gt;One migration creates an index on an existing table without &lt;code&gt;CONCURRENTLY&lt;/code&gt;. On Postgres, a plain &lt;code&gt;CREATE INDEX&lt;/code&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="n"&gt;concurrently&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="n"&gt;idx_orders_user&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CONCURRENTLY&lt;/code&gt; can't run inside a transaction, so some migration tools need it in a migration of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking your own project
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://zelsis.com" rel="noopener noreferrer"&gt;zelsis.com&lt;/a&gt;. If you maintain one of the projects above and want the details, write to &lt;a href="mailto:contact@zelsis.com"&gt;contact@zelsis.com&lt;/a&gt; and we'll send what we found.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>postgres</category>
      <category>android</category>
    </item>
  </channel>
</rss>
