<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[The Secure Cookie]]></title><description><![CDATA[Newsletter on Secure Coding and Web Security]]></description><link>https://newsletter.ferranverdes.net</link><image><url>https://substackcdn.com/image/fetch/$s_!Jyls!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9a914da-6240-4f8a-a38e-9916064fc203_832x832.png</url><title>The Secure Cookie</title><link>https://newsletter.ferranverdes.net</link></image><generator>Substack</generator><lastBuildDate>Tue, 05 May 2026 09:27:55 GMT</lastBuildDate><atom:link href="https://newsletter.ferranverdes.net/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Ferran]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[ferranverdes@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[ferranverdes@substack.com]]></itunes:email><itunes:name><![CDATA[Ferran]]></itunes:name></itunes:owner><itunes:author><![CDATA[Ferran]]></itunes:author><googleplay:owner><![CDATA[ferranverdes@substack.com]]></googleplay:owner><googleplay:email><![CDATA[ferranverdes@substack.com]]></googleplay:email><googleplay:author><![CDATA[Ferran]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[🍪#8 Fuzzing an ADK LLM-based Agent via a GitLab CI/CD Pipeline]]></title><description><![CDATA[Automating AI security testing to detect jailbreaks and report vulnerabilities directly in GitLab&#8217;s Vulnerability Report using FuzzyAI]]></description><link>https://newsletter.ferranverdes.net/p/fuzzing-an-adk-llm-based-agent-via-a-gitlab-ci-cd-pipeline</link><guid isPermaLink="false">https://newsletter.ferranverdes.net/p/fuzzing-an-adk-llm-based-agent-via-a-gitlab-ci-cd-pipeline</guid><dc:creator><![CDATA[Ferran]]></dc:creator><pubDate>Fri, 13 Mar 2026 17:08:11 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Jg9N!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15dc504d-194f-4f07-97d8-4bccb0d3148e_1418x950.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently started learning about AI more seriously, specifically about <strong>AI security</strong>, and to be honest, I got completely hooked. It actually surprised me how captivated I became, because when I was studying at university, I remember not being very enthusiastic about AI at all. Back then, it felt just like something interesting, but not really useful for everyone&#8217;s life.</p><p>Luckily, or maybe not, everything has changed. I actually wrote <a href="https://newsletter.ferranverdes.net/p/the-next-evolution-of-the-web-from-navigation-to-intent">how AI will become the core part of the websites we use every day</a>, and that made me realize that learning about AI security is not only interesting, but will likely become <strong>the most important skill</strong> in the near future for security professionals.</p><p>If you come from the AppSec world like me, you will probably notice that many things feel familiar. There are more similarities with traditional applications than you might expect, even though the fundamentals are different. This actually makes getting started much easier, so I definitely encourage you to start exploring right now.</p><p><em>Fuzzing</em> is not a new concept at all, but for some reason it confused me for quite a while because I kept mixing it up with brute force attacks. I was never sure where the line between them was, until I found definitions that finally made it click for me:</p><ul><li><p><strong>Brute force</strong> is a trial-and-error technique where an automated process tries many possible combinations in the hope of eventually guessing the correct one, for example to crack passwords, secrets, or encryption keys.</p></li><li><p><strong>Fuzzing</strong>, on the other hand, is a testing technique where random, malformed, or unexpected inputs are sent to an application in order to discover bugs, crashes, or vulnerabilities.</p></li></ul><p>When we bring this idea into the LLMs context, <em>fuzzing</em> means automatically generating large volumes of unexpected, malformed, or adversarial inputs to reveal weaknesses in the model.</p><p>Running a <em>fuzzing</em> test as part of a CI/CD pipeline makes a lot of sense. The goal is simple: find the problems before attackers do, and harden the model against things like prompt injection, data exfiltration, unsafe outputs, or misalignment. And that is exactly what the GitHub repository that I am introducing in this post is all about.</p><p>The goal of this project is to demonstrate how <em>fuzzing</em> can be applied to an ADK LLM-based agent, while fully automating the process through a GitLab CI/CD pipeline and deploying the target agent on Google Cloud using Pulumi.</p><p>In this example, I use <code>FuzzyAI</code> to generate adversarial prompts designed to jailbreak the agent, extract sensitive information, or bypass its safety controls. The responses are then evaluated using GPT-4o as a classifier, which determines whether the model behavior should be considered unsafe or policy-violating.</p><p>This is the pipeline job definition:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;yaml&quot;,&quot;nodeId&quot;:&quot;d788f3ec-99fc-41b0-99a7-f2b25e21e028&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-yaml">fuzzyai_jailbreak_scan:
  stage: fuzz
  image: python:3.12
  needs:
    - job: deploy
  variables:
    FUZZYAI_TARGET_URL: "$OLLAMA_BACKEND_URL"
    FUZZYAI_HTTP_METHOD: "POST"
  before_script:
    - cd fuzzyai
    - pip install git+https://github.com/cyberark/FuzzyAI.git@8184b96
  script:
    - fuzzyai fuzz -C config.json -e host="$(echo "$OLLAMA_BACKEND_URL" | sed -E 's|https?://||')"
    - python scripts/fuzzyai_to_gitlab_dast.py results/*/report.json gl-dast-report.json
  artifacts:
    when: always
    paths:
      - fuzzyai/gl-dast-report.json
    reports:
      dast: fuzzyai/gl-dast-report.json</code></pre></div><p>To integrate the results into the security pipeline, I added a simple script, called <code>fuzzyai_to_gitlab_dast.py</code>, that converts the findings produced by FuzzyAI into a format compatible with GitLab DAST reports. This allows all detected issues to be automatically collected and displayed in the GitLab Vulnerability Report dashboard, making LLM security testing part of the standard DevSecOps workflow.</p><p>And the result is pretty interesting. <strong>The fuzzing process</strong> <strong>actually</strong> <strong>managed to jailbreak the LLM</strong>, and the pipeline <strong>reported it as a critical vulnerability</strong> in GitLab:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Jg9N!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15dc504d-194f-4f07-97d8-4bccb0d3148e_1418x950.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Jg9N!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15dc504d-194f-4f07-97d8-4bccb0d3148e_1418x950.png 424w, https://substackcdn.com/image/fetch/$s_!Jg9N!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15dc504d-194f-4f07-97d8-4bccb0d3148e_1418x950.png 848w, https://substackcdn.com/image/fetch/$s_!Jg9N!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15dc504d-194f-4f07-97d8-4bccb0d3148e_1418x950.png 1272w, https://substackcdn.com/image/fetch/$s_!Jg9N!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15dc504d-194f-4f07-97d8-4bccb0d3148e_1418x950.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Jg9N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15dc504d-194f-4f07-97d8-4bccb0d3148e_1418x950.png" width="1418" height="950" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/15dc504d-194f-4f07-97d8-4bccb0d3148e_1418x950.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:950,&quot;width&quot;:1418,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:167109,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.ferranverdes.net/i/190849758?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15dc504d-194f-4f07-97d8-4bccb0d3148e_1418x950.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Jg9N!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15dc504d-194f-4f07-97d8-4bccb0d3148e_1418x950.png 424w, https://substackcdn.com/image/fetch/$s_!Jg9N!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15dc504d-194f-4f07-97d8-4bccb0d3148e_1418x950.png 848w, https://substackcdn.com/image/fetch/$s_!Jg9N!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15dc504d-194f-4f07-97d8-4bccb0d3148e_1418x950.png 1272w, https://substackcdn.com/image/fetch/$s_!Jg9N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15dc504d-194f-4f07-97d8-4bccb0d3148e_1418x950.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Here is the repository if you want to explore the full codebase:</p><p>&#128073; <strong>GitHub Link</strong>: <em><a href="https://github.com/ferranverdes/Fuzzing-ADK-Agent-via-GitLab-pipeline">Fuzzing-ADK-Agent-via-GitLab-pipeline</a></em></p><p>Pretty straightforward, and probably easier than you expected, right?</p><p>I definitely want to keep experimenting with ways to automate security testing for AI models, but right now I am not sure which areas are the most relevant to explore next. Any ideas in mind? I would love to hear them and consider trying them out.</p><p>Thanks for reading this post!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[The Next Evolution of the Web: From Navigation to Intent]]></title><description><![CDATA[A practical look at how websites will stop showing content and start completing tasks.]]></description><link>https://newsletter.ferranverdes.net/p/the-next-evolution-of-the-web-from-navigation-to-intent</link><guid isPermaLink="false">https://newsletter.ferranverdes.net/p/the-next-evolution-of-the-web-from-navigation-to-intent</guid><dc:creator><![CDATA[Ferran]]></dc:creator><pubDate>Wed, 17 Dec 2025 15:02:46 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/3eea8e15-76cb-489b-9434-60991e94097c_1536x951.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Looking at the evolution of the web, starting with static pages and progressing through interactive sites, mobile-first design, and platform ecosystems, it becomes clear that AI represents the next structural shift, not just another feature in the form of a highly capable chatbot.</p><p>AI has the potential to move websites beyond predefined content toward understanding intent, reasoning over context, and acting on the user&#8217;s behalf. With that in mind, here&#8217;s how I believe websites will change over the next few years.</p><h2>1. From pages to interfaces</h2><p>Websites today are mostly collections of pages you navigate. In the future, many sites will behave more like <strong>adaptive interfaces</strong>.</p><ul><li><p>Fewer menus, more <strong>conversations</strong>.</p></li><li><p>You don&#8217;t browse, you <em>ask.</em></p></li><li><p>The &#8220;homepage&#8221; becomes an intelligent entry point, not a layout.</p></li></ul><p><strong>Example:</strong></p><p>Instead of navigating listings on <strong>Booking</strong>, you say:</p><blockquote><p><em>&#8220;Plan a 5-day trip to Japan in April with food and nature focus.&#8221;</em></p></blockquote><p>The site then builds the experience on demand.</p><h2>2. AI becomes the primary user interface</h2><p>Search boxes, filters, and FAQs will fade into the background.</p><p>AI agents act as:</p><ul><li><p>Sales reps.</p></li><li><p>Customer support.</p></li><li><p>Personal assistants.</p></li></ul><p>Each user gets a <strong>customized site behavior</strong>, not just personalized content.</p><p>Two people visiting the same website may effectively see <strong>different websites</strong>, optimized for:</p><ul><li><p>Their goals.</p></li><li><p>Their skill level.</p></li><li><p>Their past behavior.</p></li><li><p>Their preferred interaction style (chat, voice, visual).</p></li></ul><p><strong>Example:</strong></p><p>Instead of searching and filtering on <strong>Amazon</strong>, you say:</p><blockquote><p><em>&#8220;I need everything for a small gaming setup under a $500 budget.&#8221;</em></p></blockquote><p>The online marketplace then selects, explains, and prepares everything in one flow.</p><h2>3. Websites become goal-driven, not content-driven</h2><p>Right now, websites <em>present information</em>. Future websites will <em>execute outcomes</em>.</p><p>Instead of:</p><ul><li><p>&#8220;Here&#8217;s our documentation&#8221;.</p></li><li><p>&#8220;Here&#8217;s our pricing page&#8221;.</p></li></ul><p>You get:</p><ul><li><p>&#8220;What are you trying to do?&#8221;.</p></li><li><p>&#8220;Let me handle that for you&#8221;.</p></li></ul><p>The site becomes a <strong>tool</strong>, not a brochure.</p><p><strong>Example:</strong></p><p>Instead of reading accounting documentation or setup guides, <strong>Holded</strong> lets users say:</p><blockquote><p><em>&#8220;Help me set up my freelance business, track income and expenses, and prepare my tax reports&#8221;</em></p></blockquote><p>And then walks them through the required steps, automates bookkeeping, and generates the necessary filings and summaries.</p><h2>4. Less navigation, more orchestration</h2><p>Traditional UI elements will shrink in importance:</p><ul><li><p>Mega menus.</p></li><li><p>Deep page hierarchies.</p></li><li><p>Manual form filling.</p></li></ul><p>AI will:</p><ul><li><p>Auto-fill.</p></li><li><p>Auto-decide defaults.</p></li><li><p>Auto-connect external services.</p></li></ul><p>The website becomes an <strong>orchestrator</strong> between APIs, data, and user intent.</p><p><strong>Example:</strong></p><p>Instead of reading instructions and filling forms, a user gives permission once on <strong>Stripe</strong>, and the platform gathers the required business data, auto-fills onboarding details, connects payments and tax services, and completes setup without repeated manual steps.</p><h2>5. Trust, identity, and verification matter more</h2><p>As AI-generated content explodes, the value of a website shifts from content to <em>credibility</em>.</p><p>Expect:</p><ul><li><p>Stronger identity verification.</p></li><li><p>Proof of authenticity (human-verified, source-backed).</p></li><li><p>Transparent AI behavior (&#8220;why this answer was generated&#8221;).</p></li></ul><p>Websites will need to <em>earn trust</em>, not just attention.</p><p><strong>Example:</strong></p><p>Platforms like <strong>Wikipedia</strong> clearly show which parts of an article were<em> AI-assisted</em>, which were<em> reviewed by humans</em>, and provide verified source trails instead of presenting raw text alone.</p><h2>6. Performance shifts from speed to intelligence</h2><p>Today we optimize:</p><ul><li><p>Load time.</p></li><li><p>Accessibility.</p></li><li><p>SEO / GEO.</p></li></ul><p>Tomorrow we optimize:</p><ul><li><p>Quality of AI reasoning.</p></li><li><p>Context awareness.</p></li><li><p>Memory across sessions.</p></li><li><p>Ethical and safe responses.</p></li></ul><p>A &#8220;slow&#8221; site that gives the right answer may outperform a fast site that forces users to think.</p><p><strong>Example:</strong></p><p>Instead of fast-loading rows, <strong>Netflix</strong> asks:</p><blockquote><p><em>&#8220;Do you want something light, intense, or comforting tonight?&#8221;</em></p></blockquote><p>And gives one strong recommendation with reasoning and stored preferences.</p><h2>7. Developers design systems, not pages</h2><p>Web development will change significantly.</p><p>Less hand-crafted UI logic.</p><p>And more:</p><ul><li><p>Prompt design.</p></li><li><p>AI behavior constraints.</p></li><li><p>Data pipelines.</p></li><li><p>Feedback loops.</p></li></ul><p>Frontend will no longer be what users see, but how AI represents choices to them.</p><p><strong>Example:</strong></p><p>Instead of static search result pages, <strong>Skyscanner</strong> interprets travel intent, analyzes live price and availability signals, surfaces trade-offs between cost, time, and convenience, and suggests next best options dynamically as conditions change.</p><h2>8. The web becomes more fragmented, and more personal</h2><p>Ironically, while AI unifies interfaces, it fragments experiences:</p><ul><li><p>Each user&#8217;s web feels unique.</p></li><li><p>Fewer shared &#8220;common&#8221; pages.</p></li><li><p>More private, ephemeral interactions.</p></li></ul><p>This may reduce viral pages, but increase deep engagement.</p><p><strong>Example:</strong></p><p><strong>LinkedIn</strong> presents differently user&#8217;s profile data depending on whether the viewer is a recruiter, founder, or investor, tailoring what&#8217;s shown based on their interests, even though everyone is using the same site.</p><h2>A short way to summarize</h2><ol><li><p><strong>Past</strong>: websites showed information.</p></li><li><p><strong>Present</strong>: websites guide users.</p></li><li><p><strong>Future</strong>: websites <em>think</em> and <em>act</em> with users.</p></li></ol><p>The biggest shift isn&#8217;t visual, it&#8217;s philosophical:</p><blockquote><p>Websites stop asking <em>&#8220;What do we want to show?&#8221; </em>and start asking <em>&#8220;What does this person want to accomplish?&#8221;</em></p></blockquote><p>This transformation redefines the web from a collection of pages into a set of intelligent systems designed to help people achieve outcomes, not just consume content.</p><p>Does this resonate with you? I&#8217;d love to hear your thoughts.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[🍪#7 A Developer-Friendly Approach to Security in CI/CD Pipelines]]></title><description><![CDATA[Illustrating secure delivery workflows with minimal friction for developers.]]></description><link>https://newsletter.ferranverdes.net/p/a-developer-friendly-approach-to-security-in-ci-cd-pipelines</link><guid isPermaLink="false">https://newsletter.ferranverdes.net/p/a-developer-friendly-approach-to-security-in-ci-cd-pipelines</guid><dc:creator><![CDATA[Ferran]]></dc:creator><pubDate>Tue, 18 Nov 2025 14:02:43 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!9s4G!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7e9454d-d4d9-465e-8a9d-8a81e0fa6c49_1247x680.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The concept of runtime environments (RTEs) was explored <a href="https://newsletter.ferranverdes.net/p/from-dev-to-prod-and-how-runtime-environments-shape-the-sdlc">in the previous post</a>, along with the distinct purposes, permissions, and boundaries each one introduces into the delivery process. As described, in DevOps, runtime environments represent all infrastructure and configuration elements required to run applications across their lifecycle. They generally include development, test, staging, and production, each fulfilling a distinct role in the software delivery pipeline, as shown below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uj0-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc3a0cb7-0bd1-4360-81aa-750912f6f047_2709x1365.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uj0-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc3a0cb7-0bd1-4360-81aa-750912f6f047_2709x1365.png 424w, https://substackcdn.com/image/fetch/$s_!uj0-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc3a0cb7-0bd1-4360-81aa-750912f6f047_2709x1365.png 848w, https://substackcdn.com/image/fetch/$s_!uj0-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc3a0cb7-0bd1-4360-81aa-750912f6f047_2709x1365.png 1272w, https://substackcdn.com/image/fetch/$s_!uj0-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc3a0cb7-0bd1-4360-81aa-750912f6f047_2709x1365.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uj0-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc3a0cb7-0bd1-4360-81aa-750912f6f047_2709x1365.png" width="1456" height="734" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cc3a0cb7-0bd1-4360-81aa-750912f6f047_2709x1365.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:734,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:221704,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.ferranverdes.net/i/179135544?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc3a0cb7-0bd1-4360-81aa-750912f6f047_2709x1365.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!uj0-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc3a0cb7-0bd1-4360-81aa-750912f6f047_2709x1365.png 424w, https://substackcdn.com/image/fetch/$s_!uj0-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc3a0cb7-0bd1-4360-81aa-750912f6f047_2709x1365.png 848w, https://substackcdn.com/image/fetch/$s_!uj0-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc3a0cb7-0bd1-4360-81aa-750912f6f047_2709x1365.png 1272w, https://substackcdn.com/image/fetch/$s_!uj0-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc3a0cb7-0bd1-4360-81aa-750912f6f047_2709x1365.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>But concepts are just theory, <strong>and being pragmatic in security is extremely important</strong>, since real constraints become only visible when trying to reach the desired security state and technology limitations usually remain hidden during the design phase. These constraints eventually require adjustments to the original plan, and a practical approach is the only way to uncover these realities before altering the direction of a project.</p><p>From this standpoint, and to make these ideas tangible and understandable, <a href="https://github.com/ferranverdes/Notes-App-with-GitLab-SCA-SAST-and-DAST">a public repository</a> has been created that implements a minimal application deployed through a fully automated CI/CD pipeline, defining different runtime environments and integrating real security controls such as SCA, SAST, IaC scanning, Secret Detection, and DAST. It also includes all instructions needed to recreate it within your own setup.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9s4G!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7e9454d-d4d9-465e-8a9d-8a81e0fa6c49_1247x680.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9s4G!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7e9454d-d4d9-465e-8a9d-8a81e0fa6c49_1247x680.png 424w, https://substackcdn.com/image/fetch/$s_!9s4G!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7e9454d-d4d9-465e-8a9d-8a81e0fa6c49_1247x680.png 848w, https://substackcdn.com/image/fetch/$s_!9s4G!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7e9454d-d4d9-465e-8a9d-8a81e0fa6c49_1247x680.png 1272w, https://substackcdn.com/image/fetch/$s_!9s4G!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7e9454d-d4d9-465e-8a9d-8a81e0fa6c49_1247x680.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9s4G!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7e9454d-d4d9-465e-8a9d-8a81e0fa6c49_1247x680.png" width="1247" height="680" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c7e9454d-d4d9-465e-8a9d-8a81e0fa6c49_1247x680.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:680,&quot;width&quot;:1247,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:351499,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.ferranverdes.net/i/179135544?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7e9454d-d4d9-465e-8a9d-8a81e0fa6c49_1247x680.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!9s4G!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7e9454d-d4d9-465e-8a9d-8a81e0fa6c49_1247x680.png 424w, https://substackcdn.com/image/fetch/$s_!9s4G!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7e9454d-d4d9-465e-8a9d-8a81e0fa6c49_1247x680.png 848w, https://substackcdn.com/image/fetch/$s_!9s4G!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7e9454d-d4d9-465e-8a9d-8a81e0fa6c49_1247x680.png 1272w, https://substackcdn.com/image/fetch/$s_!9s4G!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7e9454d-d4d9-465e-8a9d-8a81e0fa6c49_1247x680.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The project is intentionally simple, but the pipeline is intentionally complete. It demonstrates what a developer-friendly, security-aligned software delivery looks like, and more importantly, it illustrates how much easier security becomes when the SDLC is predictable, automated, and grounded in clear environment definitions.</p><p>Let&#8217;s break it down.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h1>A Minimal Notes API&#8230; with Maximum Security Visibility</h1><p>The application itself is deliberately uncomplicated: a small Express.js REST API with two endpoints:</p><ul><li><p><strong>POST </strong><code>/notes</code> &#8212; create a note</p></li><li><p><strong>GET </strong><code>/notes</code> &#8212; list existing notes</p></li></ul><p>There is no authentication, no validation, and intentionally no protections.<br>Why?<br>Because the objective is not the app, it&#8217;s the <strong>pipeline</strong>.</p><p>The code is built to reveal:</p><ul><li><p>How insecure defaults become evident when placed within a CI/CD process.</p></li><li><p>How GitLab detects vulnerabilities at multiple layers.</p></li><li><p>How cloud-native deployments can be automated securely.</p></li><li><p>How clearly defined environments shape your security posture.</p></li></ul><p>It may be viewed as a &#8220;Hello World&#8221; for DevSecOps foundations.</p><h1>The Architecture: Small App, Real Deployment Model</h1><p>The repository deploys the application to <strong>Google Cloud Run</strong>, fully orchestrated through <strong>Pulumi</strong>, using <strong>GitLab CI/CD</strong> as the automation engine.</p><p>It includes:</p><ul><li><p><strong>Three isolated environments</strong>:<br><code>dev</code>, <code>stage</code>, <code>prod</code> &#8212; each on their own GCP project.</p></li><li><p><strong>Dedicated service accounts</strong> per environment for deployment.</p></li><li><p><strong>A separate identity for DAST</strong>, so staging remains locked down.</p></li><li><p><strong>Environment-specific configuration and seeding behaviour</strong>:</p><ul><li><p><code>dev</code>: purged + seeded on each deploy.</p></li><li><p><code>stage</code>: seeded but existing data preserved.</p></li><li><p><code>prod</code>: never seeded, to protect real production data.</p></li></ul></li></ul><p>These decisions mirror the environment definitions explained in the <a href="https://newsletter.ferranverdes.net/p/from-dev-to-prod-and-how-runtime-environments-shape-the-sdlc">latest post</a>:<br>different expectations &#8594; different permissions &#8594; different controls.</p><p>This is precisely what healthy RTEs look like.</p><h1>How the Delivery Flow Is Intended to Operate</h1><p>The repository suggests a simple, clean, and predictable workflow:</p><pre><code><code>feature/* &#8594; dev &#8594; main &#8594; prod</code></code></pre><p>Each branch moves the application through the SDLC:</p><ul><li><p>Commits to <strong>dev</strong> deploy to the <strong>development environment.</strong></p></li><li><p>Merging into <strong>main</strong> deploys to <strong>staging environment</strong> and runs <strong>DAST.</strong></p></li><li><p>Merging into <strong>prod</strong> deploys to <strong>production environment.</strong></p></li></ul><p>Static scans run on every branch and MR to provide early feedback. However, since <code>main</code> is the <strong>default branch</strong>, it is the only one whose scan results appear in the <strong>GitLab Vulnerability Report Dashboard</strong>.</p><p>The <strong>main</strong> branch serves as the central and stable source of truth for the project.</p><h1>Security Scans Integrated End-to-End</h1><p>The following GitLab security analyzers run automatically:</p><ul><li><p><strong>SCA (Dependency Scanning)</strong>: identifies vulnerable third-party libraries.</p></li><li><p><strong>Secret Detection</strong>: uncovers exposed tokens and sensitive credentials.</p></li><li><p><strong>SAST (Semgrep)</strong>: detects insecure coding patterns within the source code.</p></li><li><p><strong>IaC Scanning (KICS)</strong>: flags misconfigurations in Pulumi and other infrastructure definitions.</p></li><li><p><strong>DAST (API Security)</strong>: performs live security testing against the running service in staging.</p></li></ul><p>This gives a <strong>multi-layered view</strong> of risk across:</p><ul><li><p>The codebase.</p></li><li><p>The dependencies.</p></li><li><p>The infrastructure.</p></li><li><p>The running service.</p></li></ul><p>All without requiring developer intervention and operating as non-blocking stages within the software delivery process.</p><p><strong>Security becomes frictionless once pipelines do the heavy lifting.</strong></p><h1>Access Control: IAM as a Security Boundary</h1><p>One of the most important aspects of this repo is <strong>how access to each environment is controlled</strong>:</p><ul><li><p><strong>dev</strong>: intentionally unreachable (reserved for internal developers access).</p></li><li><p><strong>stage</strong>: only reachable by the GitLab DAST service account (also reserved for internal developers access).</p></li><li><p><strong>prod</strong>: publicly accessible.</p></li></ul><p>This is a practical example of how you can use <strong>IAM-based access restrictions</strong> to enforce clear separation of concerns in the SDLC.</p><p>Most security incidents don&#8217;t occur because encryption was missing or because a fancy system wasn&#8217;t configured correctly, they happen because <strong>access control boundaries were blurry</strong>.</p><blockquote><p>&#9888;&#65039; According to the recent OWASP 2025 release, Broken Access Control remains positioned as the highest-ranked vulnerability group.</p></blockquote><h1>The Purpose Behind This Repo</h1><p>This project is not just a template, it can serve as a practical reference.</p><p>It shows how defining runtime environments clearly, and aligning CI/CD logic with those definitions, naturally reduces the friction of applying security controls.</p><p>It also demonstrates how:</p><ul><li><p>access boundaries</p></li><li><p>identity separation</p></li><li><p>predictable configurations</p></li><li><p>automation</p></li><li><p>and environment isolation</p></li></ul><p>&#8230;all contribute to making security scalable rather than painful.</p><p>Because security isn&#8217;t just about adding scanners. <strong>It&#8217;s about</strong> <strong>creating the right conditions for security to flourish</strong>.</p><h1>Explore the Repository</h1><p>I encourage you to take a closer look at the repository:</p><p>&#128073; <strong>GitHub Link:</strong> <em><a href="https://github.com/ferranverdes/Notes-App-with-GitLab-SCA-SAST-and-DAST">Notes-App-with-GitLab-SCA-SAST-and-DAST</a></em></p><p>Fork it, clone it, run it locally, inspect the pipeline, review the Pulumi code, and break it freely. It&#8217;s built for your own experimentation.</p><h1>&#127850; Thanks for Reading The Secure Cookie</h1><p>If you are enjoying these contributions to real-world application security, consider subscribing or sharing it with someone who might benefit. And if you are into great security reads, I just picked up the book &#128214; <a href="https://www.amazon.com/dp/1394171706">Alice and Bob Learn Secure Coding</a> by Tanya Janca, and I can honestly recommend diving into it.</p><p>See you in the arena, gladiators!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[🍪#6 From Dev to Prod and How Runtime Environments Shape the SDLC]]></title><description><![CDATA[Clarifying the differences between Development, Test, Staging, and Production environments.]]></description><link>https://newsletter.ferranverdes.net/p/from-dev-to-prod-and-how-runtime-environments-shape-the-sdlc</link><guid isPermaLink="false">https://newsletter.ferranverdes.net/p/from-dev-to-prod-and-how-runtime-environments-shape-the-sdlc</guid><dc:creator><![CDATA[Ferran]]></dc:creator><pubDate>Tue, 28 Oct 2025 06:01:48 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/717c81df-8aca-413f-891d-133d8ced55f2_631x223.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WkPZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35443fc0-8eb4-4e09-a6c7-fa71b15a6bb1_631x223.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WkPZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35443fc0-8eb4-4e09-a6c7-fa71b15a6bb1_631x223.png 424w, https://substackcdn.com/image/fetch/$s_!WkPZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35443fc0-8eb4-4e09-a6c7-fa71b15a6bb1_631x223.png 848w, https://substackcdn.com/image/fetch/$s_!WkPZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35443fc0-8eb4-4e09-a6c7-fa71b15a6bb1_631x223.png 1272w, https://substackcdn.com/image/fetch/$s_!WkPZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35443fc0-8eb4-4e09-a6c7-fa71b15a6bb1_631x223.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WkPZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35443fc0-8eb4-4e09-a6c7-fa71b15a6bb1_631x223.png" width="631" height="223" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/35443fc0-8eb4-4e09-a6c7-fa71b15a6bb1_631x223.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:223,&quot;width&quot;:631,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:153775,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.ferranverdes.net/i/177017196?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35443fc0-8eb4-4e09-a6c7-fa71b15a6bb1_631x223.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!WkPZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35443fc0-8eb4-4e09-a6c7-fa71b15a6bb1_631x223.png 424w, https://substackcdn.com/image/fetch/$s_!WkPZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35443fc0-8eb4-4e09-a6c7-fa71b15a6bb1_631x223.png 848w, https://substackcdn.com/image/fetch/$s_!WkPZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35443fc0-8eb4-4e09-a6c7-fa71b15a6bb1_631x223.png 1272w, https://substackcdn.com/image/fetch/$s_!WkPZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35443fc0-8eb4-4e09-a6c7-fa71b15a6bb1_631x223.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>Interpretation shapes perception, and the same principle applies to security in organizations, which is far from an exact science. Security embodies a broad spectrum of colours, and each company must choose the one that best reflects its character. It stands as a cultural mindset that extends from leadership to every team, influenced by both executive and technical management.</p><p>Sometimes, raising security levels requires making decisions that change how teams operate, and this is where that mindset becomes essential. Since technology evolves continuously, security does as well, although its changes are not inherently negative. They may demand an initial effort, but they can pay off in the long run. However, to recognise their value, it&#8217;s essential to maintain a clear vision and understand what lies beyond the immediate effort.</p><p>To put it in practical terms, today it's far easier to integrate security into applications when there is a complete SDLC fully automated within a CI/CD pipeline. A few years ago, the idea of creating a streamlined pipeline to enhance security might not have been viewed as practical, but now it&#8217;s a game changer. The reason is simple: strong and effective security checks can be seamlessly integrated, and teams that still rely on manual or fragmented workflows find it much harder to embed security, resulting in slower and less efficient delivery processes. Plus, looking ahead, AI will increasingly contribute to add and govern more of these controls.</p><p>That&#8217;s why I am drawn to organisations that treat security not as a checklist but as a core element of their identity, being taken into account in every decision from architecture to delivery. I had to defend security projects when the vision wasn&#8217;t shared at all, and despite much persistence, it was only on rare occasions that I saw a leader&#8217;s opinion shift. It was very frustrating for me. I found it extremely hard to change someone&#8217;s mind when security is seen as necessary but just a little more than costly and irritating, a colour I would not choose from the spectrum mentioned before.</p><p>Entering the technical section of this post, it&#8217;s important to notice that the key is not how many environments are defined or how they are named and used within the SDLC, but what is expected from each and which actions are permitted or restricted. These are simply my references based on my experience, which I hope you find descriptive and useful.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Understanding RTEs</h2><p>A runtime environment is the space where a program or application runs, considering both the hardware and software infrastructure required for executing the code in real-time. </p><p>In DevOps, runtime environments encompass all infrastructure and configurations involved in executing applications throughout their entire lifecycle. These environments typically include development, test, staging, and production environments, each serving a specific purpose in the software delivery pipeline:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BpQx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27db9e2b-75fd-427c-8d00-46d80ea09b7c_2709x1365.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BpQx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27db9e2b-75fd-427c-8d00-46d80ea09b7c_2709x1365.png 424w, https://substackcdn.com/image/fetch/$s_!BpQx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27db9e2b-75fd-427c-8d00-46d80ea09b7c_2709x1365.png 848w, https://substackcdn.com/image/fetch/$s_!BpQx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27db9e2b-75fd-427c-8d00-46d80ea09b7c_2709x1365.png 1272w, https://substackcdn.com/image/fetch/$s_!BpQx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27db9e2b-75fd-427c-8d00-46d80ea09b7c_2709x1365.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BpQx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27db9e2b-75fd-427c-8d00-46d80ea09b7c_2709x1365.png" width="1456" height="734" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/27db9e2b-75fd-427c-8d00-46d80ea09b7c_2709x1365.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:734,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:221704,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.ferranverdes.net/i/177017196?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27db9e2b-75fd-427c-8d00-46d80ea09b7c_2709x1365.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!BpQx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27db9e2b-75fd-427c-8d00-46d80ea09b7c_2709x1365.png 424w, https://substackcdn.com/image/fetch/$s_!BpQx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27db9e2b-75fd-427c-8d00-46d80ea09b7c_2709x1365.png 848w, https://substackcdn.com/image/fetch/$s_!BpQx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27db9e2b-75fd-427c-8d00-46d80ea09b7c_2709x1365.png 1272w, https://substackcdn.com/image/fetch/$s_!BpQx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27db9e2b-75fd-427c-8d00-46d80ea09b7c_2709x1365.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Managing and maintaining consistent and reliable runtime environments is crucial for ensuring the security, stability, scalability, and performance of applications as they progress from development to deployment serving end users.</p><h3>Development Environment</h3><p>The <strong>development environment</strong> is the first environment in software development which acts as the workspace for developers to do programming and other operations related to the creation of software. It covers from the first lines of code to all code updates. As the name suggests, this is where the development of the software takes place.</p><p>Part of the development environment also includes the Integrated Development Environment (IDE), which is a software package with extensive functions for authoring, building, testing, and debugging a program that is commonly used by software developers running on its own workstation. Common IDEs are Visual Studio Code, IntelliJ IDEA, Cursor, etc.</p><h3>Test Environment</h3><p>A <strong>test environment</strong> allows testing engineers to analyse new and changed code whether via automated or non-automated techniques. Using tests environment testers make sure that the new code will not have any impact on the existing functionality. They also ensure the quality of the code by finding any bugs and reviewing all bug fixes.</p><p>The primary focus here is testing individual components rather than the entire application, aiming to verify compatibility between old and new code. Different types of testing suggest different types of test environments, some or all of which may be virtualized to allow rapid, parallel testing to take place.</p><h3>Staging Environment</h3><p>A <strong>staging </strong>or<strong> pre-production environment</strong> is a nearly exact replica of the production environment, aiming to closely mirror the actual production environment to ensure the software works correctly.</p><p>The focus here is to test the application or software as a whole. This is where you can conduct deep tests to ensure that no problems come up in production and limit negative impact on users. Another key purpose of staging is performance testing, especially load testing, since it is often highly sensitive to environmental factors.</p><p>Depending on any regulatory factors (such as GDPR requirements) and the organization&#8217;s level of ability to anonymize data, a staging environment may include not only fake but also anonymized or complete sets of production data to closely replicate the real-world production environment.</p><p>Access to the staging environment is often limited to a small group of individuals. Only those with whitelisted emails or specific network addresses, along with the developer team, have access the application in staging.</p><p>Isolating the staging and production environments on two separate clusters and VPCs is a good practice to avoid any potential issues on production caused by the staging environment. This is not a mandatory step, but it is well recommended.</p><h4>How to create a Staging Environment</h4><ul><li><p>Create a staging environment from scratch.</p></li><li><p>Clone the production environment and create a staging environment from it.</p></li></ul><h4>What is the difference between Test Environment and Staging Environment</h4><p>The main difference between a staging environment and a testing environment is the level of similarities to the production environment. </p><p>In a staging environment, every element is updated to the latest version, closely reflecting the live site except for the changes recently pushed to the development environment. This configuration ensures that new changes are tested thoroughly to avoid unexpected disruptions when deployed to the production environment.</p><p>In a testing environment, there isn&#8217;t a strict need to update every element to match the production environment. Testing here is more targeted, focusing on specific code changes rather than a full system test, and it operates on assumptions about how other parts of the system function. This approach speeds up the testing process, as there&#8217;s no requirement to fully replicate the production setup, unlike in a staging environment.</p><h3>Production Environment</h3><p>A <strong>production </strong>or<strong> live environment</strong> is where it runs on a production instance and is officially available to real users. </p><p>When deploying a new release to production, rather than immediately deploying to all users, the release should be deployed in phases to a segment of users first to see how it performs to catch and fix any additional bugs before deploying to the rest of the users.</p><h2>Showing How This Works in Practice</h2><p>To make it easier to understand the purpose of environment definitions, I created a Git repository with a minimal web application configured under a CI/CD pipeline, on GitLab, for automatic deployment to Google Cloud. It will later integrate SCA, SAST, and DAST security practices in the following upcoming posts.</p><p>&#128073; <a href="https://github.com/ferranverdes/Notes-App-with-GitLab-SCA-SAST-and-DAST">This is the link to the repository</a>.</p><p>I encourage you to take a closer look. Let&#8217;s bring things to the arena, gladiators!</p><h2>Reading Picks</h2><p>Here are a few articles I found valuable in recent weeks:</p><ul><li><p><a href="https://cloudsecurityguy.substack.com/p/the-grc-engineers-toolkit-how-to">The GRC Engineer&#8217;s Toolkit - How To Turn Compliance Into Code</a> by <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Taimur Ijlal&quot;,&quot;id&quot;:103233323,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/f0bf6f56-8525-4e74-80e8-76b0aab8ad05_200x200.png&quot;,&quot;uuid&quot;:&quot;3d7f1377-1179-4cc4-a15a-7587e798a6bd&quot;}" data-component-name="MentionToDOM"></span></p></li><li><p><a href="https://www.thetrueengineer.com/p/success-number-of-attempts-probability">Success = (number of attempts) &#215; (probability of success each time)</a> by <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Adlet Balzhanov&quot;,&quot;id&quot;:288535731,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!-kq4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7477bf7a-c676-4108-bd47-cd7697318a26_1024x1024.png&quot;,&quot;uuid&quot;:&quot;3d3229be-bffb-491e-be25-30f7c7a2fd90&quot;}" data-component-name="MentionToDOM"></span> </p></li><li><p>&#128214; I just bought the book <a href="https://www.amazon.com/dp/1394171706">Alice and Bob Learn Secure Coding</a>! by Tanya Janca.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[🍪#5 The Final Step to Secure File Uploads]]></title><description><![CDATA[Managing file size, storage, and permissions to build resilient and secure upload features.]]></description><link>https://newsletter.ferranverdes.net/p/the-final-step-to-secure-file-uploads</link><guid isPermaLink="false">https://newsletter.ferranverdes.net/p/the-final-step-to-secure-file-uploads</guid><dc:creator><![CDATA[Ferran]]></dc:creator><pubDate>Thu, 16 Oct 2025 15:16:09 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!-UgW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad889d0e-d06c-4e2b-9ee5-574ba5c3b5df_906x314.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-UgW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad889d0e-d06c-4e2b-9ee5-574ba5c3b5df_906x314.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-UgW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad889d0e-d06c-4e2b-9ee5-574ba5c3b5df_906x314.png 424w, https://substackcdn.com/image/fetch/$s_!-UgW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad889d0e-d06c-4e2b-9ee5-574ba5c3b5df_906x314.png 848w, https://substackcdn.com/image/fetch/$s_!-UgW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad889d0e-d06c-4e2b-9ee5-574ba5c3b5df_906x314.png 1272w, https://substackcdn.com/image/fetch/$s_!-UgW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad889d0e-d06c-4e2b-9ee5-574ba5c3b5df_906x314.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-UgW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad889d0e-d06c-4e2b-9ee5-574ba5c3b5df_906x314.png" width="906" height="314" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ad889d0e-d06c-4e2b-9ee5-574ba5c3b5df_906x314.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:314,&quot;width&quot;:906,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:152061,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.ferranverdes.net/i/175511901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad889d0e-d06c-4e2b-9ee5-574ba5c3b5df_906x314.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!-UgW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad889d0e-d06c-4e2b-9ee5-574ba5c3b5df_906x314.png 424w, https://substackcdn.com/image/fetch/$s_!-UgW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad889d0e-d06c-4e2b-9ee5-574ba5c3b5df_906x314.png 848w, https://substackcdn.com/image/fetch/$s_!-UgW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad889d0e-d06c-4e2b-9ee5-574ba5c3b5df_906x314.png 1272w, https://substackcdn.com/image/fetch/$s_!-UgW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad889d0e-d06c-4e2b-9ee5-574ba5c3b5df_906x314.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This is the final piece of the puzzle in our journey to secure file uploads. After covering <a href="https://newsletter.ferranverdes.net/p/file-name-sanitization">file name sanitization</a>, <a href="https://newsletter.ferranverdes.net/p/why-file-type-validation-is-always-an-untrusted-check">file type validation</a>, and <a href="https://newsletter.ferranverdes.net/p/turning-wafs-into-a-virustotal-like-platform-for-file-content-validation">file content inspection</a>, the focus now shifts to ensuring the upload infrastructure itself is secure through <strong>robust resource limit management</strong> and <strong>strict permissions/access controls</strong>.</p><h2>Why Resource Limits Matter</h2><p>Even perfectly validated files can become a problem when there are no boundaries. Attackers don&#8217;t always need to exploit a vulnerability, sometimes they just need scale:</p><ul><li><p>Oversized files can <strong>exhaust resources</strong>, such as RAM or CPU, degrading performance and availability. Attackers may repeatedly upload them to overwhelm system resources.</p></li><li><p>Heavy uploads can <strong>consume bandwidth</strong>, affecting network performance for legitimate users.</p></li><li><p>Numerous files can <strong>complicate storage</strong> management, increasing maintenance needs and making backups and recovery processes slower and more error-prone.</p></li></ul><p>When applications allow users to upload without limits, two key risks emerge:</p><ul><li><p><strong>Denial of Service (DoS)</strong>: flooding a system with excessive or oversized uploads can exhaust available resources instead of exploiting a weakness.</p></li><li><p><strong>Cost spikes</strong>: on cloud platforms that bill by storage, bandwidth, or processing time, a few malicious users can quickly generate massive expenses.</p></li></ul><h2>Resource Limit Management</h2><p>Effective resource limit management requires enforcing technical controls over the size of files, the total number of files a user can upload, and the rate of upload and download requests.</p><h3>File Size Limits</h3><p>Every upload endpoint should enforce a maximum file size. Without this control, an oversized file can freeze your app while it tries to process or store it.</p><p>A simple way to handle this in Node.js is with <code>multer</code>, which supports size limits natively:</p><pre><code>const multer = require(&#8221;multer&#8221;);

const upload = multer({
  ...
  limits: { fileSize: 5 * 1024 * 1024 } // 5 MB
});</code></pre><p>When users exceed this size, the request is automatically rejected, keeping memory and disk usage predictable.</p><p>Beyond security, size limits also help protect user experience by preventing long upload times and reducing failures in slow connections.</p><blockquote><p>&#128161; Tip: Define limits based on your actual business needs.</p></blockquote><h3>Total Upload Limits</h3><p>Restricting the size of a single file is not enough. An attacker could automate uploads of countless small files, eventually filling up your storage and degrading system performance. Even if each upload is small and passes validation, the cumulative effect can overwhelm storage, backups, and indexing processes.</p><p>A reliable method is to also monitor the number of uploads or the total storage space consumed by each user. When either threshold is reached, the application should temporarily block new uploads until space is freed or the quota is increased. This approach enforces fair resource usage across users, prevents storage abuse, and helps maintain long-term system reliability.</p><h3>Upload and Download Rate Limiting</h3><p>Rate limiting is a necessary measure that imposes restrictions on the number of upload and download requests a user can make within a short period to prevent excessive data transfers. This control helps prevent any single user from flooding the system with numerous requests, which is crucial for mitigating Denial of Service (DoS) attacks.</p><p>Here is a simple Express example using the <code>express-rate-limit</code> npm package to control the number of uploads allowed per time window:</p><pre><code>const { rateLimit } = require(&#8221;express-rate-limit&#8221;);

const uploadLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes window
  max: 100, // Limit each IP to 100 requests per windowMs
  message: &#8220;Too many uploads, please wait a bit.&#8221;
});

app.post(&#8221;/upload&#8221;, uploadLimiter, (req, res) =&gt; {
  // File upload logic here
});</code></pre><p>Whenever authentication is available, base rate limits on the user&#8217;s session rather than on IP addresses. Also note that the IP received by the application often belongs to an intermediary element in the infrastructure rather than the actual client.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>File Storage Location</h2><p>Before setting up uploads, it&#8217;s important to decide where the files will live. The storage location directly impacts both security and performance.</p><p>Using a third-party storage service such as Google Cloud Storage is often the preferred option. These services typically come with robust, industry-standard security features, including encryption (both at rest and in transit), secure APIs, role-based access control (RBAC), automated updates, built-in redundancy, detailed audit logs, and compliance with various regulations like GDPR, HIPAA, or SOC2.</p><p>When a third-party service is not an option, consider deploying a dedicated server for file storage, isolated from the main web application. This approach provides complete segregation of duties between the application handling user interactions and upload requests from the server managing file storage, thereby reducing the impact of potential vulnerabilities.</p><p>If neither a storage service nor a separate server is possible and files must reside on the same host, make sure they are placed outside the webroot directory to prevent direct web access and reduce exploitation risk.</p><h2>Permissions and Access Control</h2><p>Once files are stored, the next question is: who can access them?</p><p>Regardless of whether files are kept in cloud object storage or on a self-hosted server, the same principle applies: only authorized users should be able to upload, view, or download them.</p><h3>Authenticate and Authorize Every Action</h3><p>When users are permitted to upload files, ensure they can only:</p><ul><li><p>Access their own uploaded content.</p></li><li><p>Retrieve files via validated application endpoints, not through direct public URLs.</p></li></ul><p>Even when files are stored in a managed service such as Google Cloud Storage, all access should be managed through your web application, where authorisation, validation, and logging can take place.</p><p>A common mistake is serving files from a publicly accessible path, for instance:</p><ul><li><p>A local web server directory like <code>/uploads/</code>.</p></li><li><p>Or an open storage bucket such as <code>https://storage.googleapis.com/my-bucket/uploads/...</code></p></li></ul><p>If filenames are predictable, attackers can easily enumerate and retrieve private files.</p><p>Instead of exposing direct storage URLs, use an internal handler in your backend to control all file access. This handler acts as a controlled gateway that verifies permissions, logs access, and only then issues a temporary signed URL for the user to download the file.</p><p>This design ensures that every download request is authenticated, authorised, and traceable, while the underlying storage such as Google Cloud Storage remains private and isolated from public exposure:</p><pre><code>const { Storage } = require(&#8221;@google-cloud/storage&#8221;);
const storage = new Storage();
const bucket = storage.bucket(&#8221;my-secure-bucket&#8221;);

app.get(&#8221;/download/:id&#8221;, authMiddleware, async (req, res) =&gt; {
  const file = await db.getFile(req.params.id);

  if (!file || file.ownerId !== req.user.id) {
    res.status(403).json({ message: &#8220;Forbidden&#8221; });
    return;
  }

  // Generate a signed URL valid for 2 minutes
  const [url] = await bucket.file(file.storedName).getSignedUrl({
    version: &#8220;v4&#8221;,
    action: &#8220;read&#8221;,
    expires: Date.now() + 2 * 60 * 1000,
  });

  res.json({ downloadUrl: url });
});</code></pre><p>In this pattern, the <code>/download/:id</code> route functions as the internal handler. It performs authentication through middleware, enforces authorisation by verifying file ownership, and generates a short-lived signed URL to provide secure, time-limited access.</p><p>This approach keeps files private within the storage service while users access them safely through verifiable and expiring links that do not reveal internal bucket paths or filenames.</p><blockquote><p>&#128161; Tip: When using signed URLs, keep expiration times short and include user or session identifiers in logs.</p></blockquote><h3>Keep Filesystem Permissions Tight</h3><p>When files are stored on a regular server such as a Linux host or a self-managed instance, permissions become a critical layer of defence. Properly configured filesystem permissions help ensure that uploaded files cannot be executed or accessed by unauthorised users, even if a vulnerability exists elsewhere in the application.</p><p>Uploaded files should never be granted execute permissions. In most cases, the application process only needs to read and write files like images, documents, or logs. Executing them should be strictly disallowed.</p><p>Additionally, not every file needs both read and write permissions. For instance, if the application only writes files that are later processed or archived, write-only permissions can further reduce the attack surface.</p><p>Equally important, ownership of the upload directory should also be limited to the non-privileged user running the application (for example, <code>nodeapp</code>), preventing other system accounts from accessing or modifying those files:</p><pre><code>sudo mkdir -p /srv/uploads
sudo chown nodeapp:nodeapp /srv/uploads
sudo chmod 700 /srv/uploads</code></pre><p>This configuration ensures that only the application user can read or write to the directory and that no one can execute files within it.</p><h2>Reading Picks</h2><p>Here are a few articles I found valuable in recent weeks:</p><ul><li><p><a href="https://www.toxsec.com/p/hacking-jwts-a-practical-guide">Hacking JWTs: A Practical Guide</a> by <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;ToxSec&quot;,&quot;id&quot;:8759131,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7223f8a0-d14f-456c-add5-c22e3795a6dc_500x500.png&quot;,&quot;uuid&quot;:&quot;64e828c0-1fc3-46e3-8984-48cd7cf847af&quot;}" data-component-name="MentionToDOM"></span></p></li><li><p><a href="https://newsletter.francofernando.com/p/what-to-do-before-refactoring">What to do before refactoring</a> by <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Franco Fernando&quot;,&quot;id&quot;:47169986,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/58a41b86-1e25-4bd0-a448-138d50731db4_800x800.png&quot;,&quot;uuid&quot;:&quot;8a37f578-f71e-414c-8111-e8bcfc601741&quot;}" data-component-name="MentionToDOM"></span></p></li><li><p><a href="https://newsletter.systemdesignclassroom.com/p/to-cache-or-not-to-cache">To Cache or Not to Cache</a> by <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Raul Junco&quot;,&quot;id&quot;:98661477,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/45a92f5e-1e2e-4dfa-9ff3-45fc5ad0c57e_612x612.png&quot;,&quot;uuid&quot;:&quot;ff782d46-31cd-4ec1-96a9-21824e42d71c&quot;}" data-component-name="MentionToDOM"></span></p></li><li><p><a href="https://github.com/zakirullin/cognitive-load">Cognitive load is what matters</a> by Artem Zakirullin</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Your Company Needs More (Good) Digital Nomads]]></title><description><![CDATA[Why letting your team work from paradise might be smarter than you think.]]></description><link>https://newsletter.ferranverdes.net/p/your-company-needs-good-digital-nomads</link><guid isPermaLink="false">https://newsletter.ferranverdes.net/p/your-company-needs-good-digital-nomads</guid><dc:creator><![CDATA[Ferran]]></dc:creator><pubDate>Thu, 09 Oct 2025 05:01:52 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!TX9w!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28adc17e-f7c8-4118-8d11-cba724f3a1db_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It&#8217;s been five years since I started living as a digital nomad, not full-time though, but usually during Europe&#8217;s winter months. I&#8217;ve lived in several countries, including Vietnam, Indonesia, Philippines, Thailand, Sri Lanka, and Morocco. Why? Because I love surfing, and that&#8217;s something I simply can&#8217;t do in my hometown.</p><p>It&#8217;s been an incredible journey of learning and growth. I&#8217;ve met so many people, made great friends, and most importantly, <strong>noticed a big difference in professional happiness</strong>. Most people living permanently in Europe, when I ask them how they feel about their jobs, usually answer, &#8220;I&#8217;m doing okay&#8221;. But when I ask digital nomads the same question, their answers are often filled with enthusiasm, &#8220;I&#8217;m really happy with it.&#8221;</p><p>And you know what? They&#8217;re often doing pretty similar tech jobs.</p><p>I realized that what truly makes the difference is perspective, not the job itself, but the perspective of working with the freedom to shape your own lifestyle. It&#8217;s a completely different mindset when your job becomes the key that enables you to live the life you want.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TX9w!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28adc17e-f7c8-4118-8d11-cba724f3a1db_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TX9w!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28adc17e-f7c8-4118-8d11-cba724f3a1db_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!TX9w!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28adc17e-f7c8-4118-8d11-cba724f3a1db_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!TX9w!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28adc17e-f7c8-4118-8d11-cba724f3a1db_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!TX9w!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28adc17e-f7c8-4118-8d11-cba724f3a1db_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TX9w!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28adc17e-f7c8-4118-8d11-cba724f3a1db_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/28adc17e-f7c8-4118-8d11-cba724f3a1db_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2479822,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.ferranverdes.net/i/175422356?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28adc17e-f7c8-4118-8d11-cba724f3a1db_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!TX9w!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28adc17e-f7c8-4118-8d11-cba724f3a1db_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!TX9w!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28adc17e-f7c8-4118-8d11-cba724f3a1db_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!TX9w!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28adc17e-f7c8-4118-8d11-cba724f3a1db_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!TX9w!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F28adc17e-f7c8-4118-8d11-cba724f3a1db_1536x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>For many digital nomads I know, including myself, work becomes our only real responsibility while abroad. When we travel to live and work in other countries, we fortunately leave behind most daily chores, such as grocery shopping, cooking, washing dishes or clothes, running errands, cleaning the house, or repairing things, because we can usually find affordable services that handle those tasks for us. Our main focus becomes our work. The rest of your day is open for you to spend however you like.</p><p>That&#8217;s where the motivation comes from. Your tech job becomes the source of this upgraded lifestyle. Everything you enjoy is made possible by the European income you earn from it. You don&#8217;t want to lose that; in fact, quite the opposite, you want to be productive. You want to complete your projects, support your colleagues, and make both clients and managers happy, because your personal happiness directly depends on the quality of your work. That&#8217;s why I&#8217;ve seen so many digital nomads approach their jobs with such strong motivation.</p><p>I&#8217;ve been to tropical coworking spaces that were quiet as a tomb, yet felt like they had two completely different sides. I was even shocked at first, everyone was deeply focused, working hard, barely talking. I thought they just weren&#8217;t social. But later, at social events, I realized people were friendly, open, and eager to connect. It wasn&#8217;t that they were antisocial, but they simply were taking their work seriously. Honestly, I&#8217;ve never experienced such a productive atmosphere in any traditional office that I&#8217;ve been.</p><p>Now, I&#8217;m not saying everyone in your company should start digital nomading. Of course, some people can&#8217;t do it, others simply wouldn&#8217;t enjoy it, and human connection remains essential, particularly when it comes to teamwork and building strong relationships. Face-to-face interaction helps people bond and collaborate better. There&#8217;s no doubt about that. So, in my opinion, the key for any company is to find the right balance.</p><p>And by balance, I don&#8217;t mean a &#8220;three days in the office, two days at home&#8221; policy. That&#8217;s not the kind of remote work I&#8217;m talking about, it barely changes the work experience and fails to provide the real advantages of what I mentioned. I&#8217;m referring to allowing people to relocate for two or three months to experience a different lifestyle and explore different routines.</p><p>So what do you think? Could letting people work abroad for a couple of months a year make them happier and more motivated, or do you think it would cause more challenges than benefits? I&#8217;d love to hear your thoughts.</p><h2>Reading Picks</h2><p>Here are a few articles I found valuable in recent weeks:</p><ul><li><p><a href="https://www.outputtheory.com/p/the-simple-habit-that-lowers-stress?r=34xvu1">The Simple Habit That Lowers Stress, Boosts Energy, and Improves Health</a> by <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Tobias Winkler&quot;,&quot;id&quot;:355775399,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/75486779-362a-4295-8033-1ef8d9db8dba_808x808.jpeg&quot;,&quot;uuid&quot;:&quot;1fceef6b-a82e-4167-afd4-b554abd697bd&quot;}" data-component-name="MentionToDOM"></span> </p></li><li><p><a href="https://pamelarubiodlg.substack.com/p/how-to-multiply-your-hapiness">How to multiply your Hapiness</a> by <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Life but Curated by PR&quot;,&quot;id&quot;:82524474,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a935ba06-ad8a-4789-85ee-e500fb2ea248_1320x1320.jpeg&quot;,&quot;uuid&quot;:&quot;d8a73f7a-6b20-4162-82f4-84e646cab2be&quot;}" data-component-name="MentionToDOM"></span></p></li><li><p><a href="https://letters.thedankoe.com/p/12-rules-to-change-your-life-in-12">12 rules to change your life in 12 months</a> by <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;DAN KOE&quot;,&quot;id&quot;:41011297,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7591b09e-6d83-4960-a71c-e2060766c42a_728x728.jpeg&quot;,&quot;uuid&quot;:&quot;57c31fcd-3ea3-4d40-999f-8e202b97e46a&quot;}" data-component-name="MentionToDOM"></span> </p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[🍪#4 Turning WAFs into a VirusTotal-like Platform for File Content Validation]]></title><description><![CDATA[As a security engineer, working with low-budget projects has sometimes driven my growth.]]></description><link>https://newsletter.ferranverdes.net/p/turning-wafs-into-a-virustotal-like-platform-for-file-content-validation</link><guid isPermaLink="false">https://newsletter.ferranverdes.net/p/turning-wafs-into-a-virustotal-like-platform-for-file-content-validation</guid><dc:creator><![CDATA[Ferran]]></dc:creator><pubDate>Thu, 02 Oct 2025 14:02:10 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!tw8j!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab2f198-ea84-4d4c-8f4f-87bfcdfc0fdc_756x692.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tw8j!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab2f198-ea84-4d4c-8f4f-87bfcdfc0fdc_756x692.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tw8j!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab2f198-ea84-4d4c-8f4f-87bfcdfc0fdc_756x692.png 424w, https://substackcdn.com/image/fetch/$s_!tw8j!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab2f198-ea84-4d4c-8f4f-87bfcdfc0fdc_756x692.png 848w, https://substackcdn.com/image/fetch/$s_!tw8j!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab2f198-ea84-4d4c-8f4f-87bfcdfc0fdc_756x692.png 1272w, https://substackcdn.com/image/fetch/$s_!tw8j!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab2f198-ea84-4d4c-8f4f-87bfcdfc0fdc_756x692.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tw8j!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab2f198-ea84-4d4c-8f4f-87bfcdfc0fdc_756x692.png" width="334" height="305.72486772486775" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fab2f198-ea84-4d4c-8f4f-87bfcdfc0fdc_756x692.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:692,&quot;width&quot;:756,&quot;resizeWidth&quot;:334,&quot;bytes&quot;:741916,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.ferranverdes.net/i/174994005?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab2f198-ea84-4d4c-8f4f-87bfcdfc0fdc_756x692.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!tw8j!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab2f198-ea84-4d4c-8f4f-87bfcdfc0fdc_756x692.png 424w, https://substackcdn.com/image/fetch/$s_!tw8j!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab2f198-ea84-4d4c-8f4f-87bfcdfc0fdc_756x692.png 848w, https://substackcdn.com/image/fetch/$s_!tw8j!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab2f198-ea84-4d4c-8f4f-87bfcdfc0fdc_756x692.png 1272w, https://substackcdn.com/image/fetch/$s_!tw8j!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffab2f198-ea84-4d4c-8f4f-87bfcdfc0fdc_756x692.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><a href="https://newsletter.ferranverdes.net/p/why-file-type-validation-is-always-an-untrusted-check">File type validation</a> won&#8217;t tell you whether a file is what it claims to be, as it can be easily spoofed through misleading extensions, manipulated magic number signatures, or altered headers. A valid approach to determine what kind of file we are dealing with is to inspect the file&#8217;s actual content and make decisions based on that analysis. However, implementing such a process as a developer introduces multiple security considerations and becomes infeasible, being complex, time-consuming, and often unreliable.</p><p>The most effective path is to rely on dedicated platforms that offer specialised capabilities for file analysis, such as VirusTotal. They provide an easier and straightforward solution for file content validation, but this immediately raises a recurring concern for organizations: [more] subscription costs.</p><p>To be honest, subscription cost challenges are something I often see among companies in Spain. Even though many SaaS platforms and service providers claim to offer competitive prices across Europe, these conditions may not always apply to the southern regions, where available budgets are often lower compared with companies located in northern territories.</p><p>As a result, many engineers I&#8217;ve worked with tend to look for the cheapest or free option available, which managers often approve to move forward. Still, file content validation must be handled carefully, as it it may result in false trust or sensitive data being exposed. For example, when using the VirusTotal platform without a paid subscription, any file you upload can be accessed by subscribed users, which obviously it&#8217;s a red flag.</p><p>This is where the real challenge begins. The company chooses not to invest in a specialized service, yet still expects you to deliver a robust implementation for file content validation.</p><p>Even though it is far from my preferred approach, there is an alternative that can still be applied to move closer to the goal. Keep in mind that simplicity will be crucial in this scenario, because if the process is not straightforward, developers will struggle, introduce errors, and may choose not to adopt it widely.</p><p>A pragmatic solution is: to adapt the existing WAF in the infrastructure into a basic file content validation tool by adding a lightweight application that offers a graphical interface, an API endpoint, or both, enabling users and other corporate applications to use it as a specialized service.</p><p>Several Web Application Firewalls (WAFs) incorporate file malware detection features in their file upload protection or content scanning modules that could be leveraged to satisfy the file content validation requirements. These modules may offer limited functionality compared to specialized services like VirusTotal, but this approach can demonstrate value gradually and eventually help convince management to finally adopt a full professional service.</p><p>Cloud-based WAFs, as opposed to on-premises appliance-based WAFs, are more likely to provide this feature while streamlining the setup for implementation.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Using FortiWeb Cloud as a File Scanning Platform</h2><p>FortiWeb Cloud is designed to protect web applications against a wide range of threats. When properly configured, it can also scan uploaded files to detect malware, greyware, malicious scripts, and other harmful content, ensuring that a file is legitimate and safe to use.</p><p>To achieve this, it uses a multi-layered scanning approach that combines signature-based detection and behavioral analysis for comprehensive protection.</p><ul><li><p><strong>Signature-based detection</strong> compares uploaded files against a database of known malware signatures. FortiWeb Cloud uses up-to-date virus definitions and threat intelligence to keep detection accurate and reliable.</p></li><li><p><strong>Behavioral analysis (via FortiSandbox)</strong> runs files in an isolated environment to observe their real-time behavior. This makes it possible to identify advanced threats such as zero-day malware or sophisticated attacks that static methods may miss.</p></li></ul><p>Together, these mechanisms offer greater assurance that harmful content in files is detected before it can compromise systems or applications.</p><h3>Submitting Files for Scanning</h3><p>The idea behind this implementation is to provide a simple graphical interface or API endpoint that accepts files for evaluation through the WAF&#8217;s scanning features. The solution is expected to return a success status code only when the files are legitimate. It should act as a middleware layer, standardising received submissions into formats supported by FortiWeb Cloud and presenting error messages in a clearer, more digestible way, for example, explicitly flagging when a file exceeds the maximum size allowed for analysis.</p><h4>File upload via multipart/form-data</h4><p>The <code>multipart/form-data</code> format is the most common and suggested approach for file uploads, as it efficiently handles binary data. FortiWeb Cloud can process and scan files directly from this format, requiring no additional steps. </p><pre><code>POST /upload HTTP/2
Host: domain.tbl
User-Agent: Mozilla/5.0 (compatible; MSIE 11.0; Windows; Windows NT 6.2; Win64; x64; en-US Trident/7.0)
Accept-Encoding: gzip, deflate, br, zstd
Content-Type: multipart/form-data; boundary=---------------------------41762806061171117218568726803
Content-Length: 656499
Connection: keep-alive

-----------------------------41762806061171117218568726803
Content-Disposition: form-data; name=&#8221;email&#8221;

johndoe@domain.tbl
-----------------------------41762806061171117218568726803
Content-Disposition: form-data; name=&#8221;file&#8221;; filename=&#8221;landscape.png&#8221;
Content-Type: image/png

[binary file data]

-----------------------------41762806061171117218568726803--</code></pre><p>The JavaScript code below uses the <code>axios</code> package to send an HTTP request as the example shown above:</p><pre><code>const sendFile = (formFile) =&gt; {
  const formData = new FormData();
  formData.append(&#8221;file&#8221;, formFile);

  return axios.post(&#8221;/upload&#8221;, formData, {
    headers: {
      &#8220;Content-Type&#8221;: &#8220;multipart/form-data&#8221;
    }
  });
};</code></pre><p>FortiWeb Cloud can also scan files that are base64-encoded and embedded in JSON objects.</p><h2>Whatever the Method, Validate File Content for Safety</h2><p>File content validation is the third essential block to address when securing file upload features in web applications, alongside file name sanitization, file type validation, and resource limit enforcement.</p><p>Files such as PDFs, office documents, and media files can contain various forms of malicious content, including hidden scripts, obfuscated payloads, or parser exploits. In some cases, uploads are deliberately crafted for phishing, disguised as invoices or reports to trick users into disclosing sensitive data.</p><p>When it comes to media files, OWASP recommends techniques such as image rewriting to remove malicious code injected into images. For Microsoft documents, certain validation libraries can also be employed to help ensure the content is legitimate.</p><p>The specific approach you choose to implement file content validation is less important than ensuring the process is effective. Analysing a file&#8217;s structure, using certain validation libraries, or applying rewriting techniques can be more complex and time-consuming, but remain entirely valid.</p><p>At the end of the day, the goal lies in adopting a validation process that ensures file content is safe before it reaches your systems or users.</p><h2>Reading Picks</h2><p>Here are a few articles I found valuable in recent weeks:</p><ul><li><p><a href="https://newsletter.francofernando.com/p/what-to-do-before-refactoring">What to do before refactoring</a> by <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Franco Fernando&quot;,&quot;id&quot;:47169986,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/58a41b86-1e25-4bd0-a448-138d50731db4_800x800.png&quot;,&quot;uuid&quot;:&quot;0a3d84dc-374e-4d93-ab8f-f99e3930a808&quot;}" data-component-name="MentionToDOM"></span></p></li><li><p><a href="https://newsletter.systemdesignclassroom.com/p/to-cache-or-not-to-cache">To Cache or Not to Cache</a> by <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Raul Junco&quot;,&quot;id&quot;:98661477,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/45a92f5e-1e2e-4dfa-9ff3-45fc5ad0c57e_612x612.png&quot;,&quot;uuid&quot;:&quot;db406d30-801a-4f50-9cd0-fa09b5cf6182&quot;}" data-component-name="MentionToDOM"></span> </p></li><li><p><a href="https://github.com/zakirullin/cognitive-load">Cognitive load is what matters</a> by Artem Zakirullin</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[🍪#3 Why File Type Validation is Always an Untrusted Check]]></title><description><![CDATA[Attackers turn file type validation into an easy bypass.]]></description><link>https://newsletter.ferranverdes.net/p/why-file-type-validation-is-always-an-untrusted-check</link><guid isPermaLink="false">https://newsletter.ferranverdes.net/p/why-file-type-validation-is-always-an-untrusted-check</guid><dc:creator><![CDATA[Ferran]]></dc:creator><pubDate>Thu, 25 Sep 2025 09:02:04 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/26b2fef4-8a95-4958-992a-e1421d09f8f6_911x186.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!e_8K!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef8a3ba-cb9f-4860-ad32-62f9b03b2c0e_911x186.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!e_8K!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef8a3ba-cb9f-4860-ad32-62f9b03b2c0e_911x186.png 424w, https://substackcdn.com/image/fetch/$s_!e_8K!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef8a3ba-cb9f-4860-ad32-62f9b03b2c0e_911x186.png 848w, https://substackcdn.com/image/fetch/$s_!e_8K!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef8a3ba-cb9f-4860-ad32-62f9b03b2c0e_911x186.png 1272w, https://substackcdn.com/image/fetch/$s_!e_8K!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef8a3ba-cb9f-4860-ad32-62f9b03b2c0e_911x186.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!e_8K!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef8a3ba-cb9f-4860-ad32-62f9b03b2c0e_911x186.png" width="911" height="186" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5ef8a3ba-cb9f-4860-ad32-62f9b03b2c0e_911x186.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:186,&quot;width&quot;:911,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40119,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.ferranverdes.net/i/174330534?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef8a3ba-cb9f-4860-ad32-62f9b03b2c0e_911x186.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!e_8K!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef8a3ba-cb9f-4860-ad32-62f9b03b2c0e_911x186.png 424w, https://substackcdn.com/image/fetch/$s_!e_8K!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef8a3ba-cb9f-4860-ad32-62f9b03b2c0e_911x186.png 848w, https://substackcdn.com/image/fetch/$s_!e_8K!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef8a3ba-cb9f-4860-ad32-62f9b03b2c0e_911x186.png 1272w, https://substackcdn.com/image/fetch/$s_!e_8K!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef8a3ba-cb9f-4860-ad32-62f9b03b2c0e_911x186.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>File type validation in upload features is usually based on three indicators: the <strong>Content-Type</strong> header, the <strong>Magic Number</strong> signature, and the <strong>File Extension</strong>. However, none of them provide complete trust from a security perspective.</p><p>At best, these checks serve as superficial filters to quickly discard obvious mismatches, but they cannot guarantee that the file is what it claims to be. Extensions can be renamed, headers can be spoofed, and magic numbers can be forged. </p><p>Let&#8217;s break down their practical role and show how they can be used wisely, meaning as quick filters that can simplify validation without giving them more trust than they deserve.</p><h2>File Type Validation via the Content-Type Header</h2><p>The <code>Content-Type</code> header is used to indicate the original MIME type (e.g., image/png, text/plain, application/pdf) of the resource prior to any content encoding applied before transmission.</p><p>MIME, short for Multipurpose Internet Mail Extensions, is a standard developed in the early 1990s to enable emails to include multimedia content and other binary files, and it is also employed on the web to define the nature of the data in the message body, the encoding applied, and how it should be processed or displayed:</p><pre><code>HTTP/1.1 200 OK
Content-Type: multipart/form-data; boundary="ExampleBoundary"

--ExampleBoundary
Content-Disposition: form-data; name="text"

Here is the text you were looking for.

--ExampleBoundary
Content-Disposition: form-data; name="file"; filename="example.jpg"
Content-Type: image/jpeg

[binary JPEG data]

--ExampleBoundary--</code></pre><p>Validating file types using the MIME type from the <code>Content-Type</code> header is unreliable for security because the header is provided by the client and can be trivially spoofed&#8212;even though some libraries and packages rely on it to assert that an upload matches the expected type.</p><p>As example, the following implementation uses <code>multer</code> to handle file uploads, which identifies the file type via the received <code>Content-Type</code> HTTP header, and mistakenly uses it as the basis for a security check:</p><pre><code>const ALLOWED_TYPES = ["image/jpeg", "image/png"];

const upload = multer({
  ...
});

app.post("/upload", upload.single("file"), (req, res) =&gt; {
  if (!req.file) {
    res.status(400).json({ message: "No file uploaded" });
    return;
  }

  const { mimetype } = req.file;

  if (!ALLOWED_TYPES.includes(mimetype)) {
    res.status(400).json({ message: "Unexpected file type" });
    return;
  }

  res.send("File uploaded successfully");
});</code></pre><h3>How to Manipulate the Content-Type Header</h3><p>Malicious users can manipulate the <code>Content-Type</code> header in HTTP requests to bypass validation that relies on this header to determine the file type. The following <code>curl</code> command offers a straightforward way to demonstrate how to send a malicious PHP file while spoofing the <code>Content-Type</code> header to appear as a <code>jpeg</code> file:</p><pre><code>curl -F "file=@malicious.php;type=image/jpeg" https://domain.tbl/upload</code></pre><p>If the server relies solely on the <code>Content-Type</code> header for validation, this request will be accepted and the file treated as a JPEG despite containing PHP code.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>File Type Validation via the Magic Number</h2><p>The magic number is a unique sequence of bytes located at the beginning of a file&#8217;s content that is used to identify the file type, according to a <a href="https://en.wikipedia.org/wiki/List_of_file_signatures">list of file signatures</a>. These bytes serve as a signature for the file, allowing the operating system or applications to determine its type, even without relying on the file extension:</p><ul><li><p><code>jpeg (jpg)</code> files start with <code>FF D8 FF DB</code> (corresponding to <code>&#255;&#216;&#255;&#219;</code>).</p></li><li><p><code>png</code> files start with <code>89 50 4E 47 0D 0A 1A 0A</code> (corresponding to <code>&#8240;PNG&#9229;&#9226;&#9242;&#9226;</code>).</p></li><li><p><code>pdf</code> files start with <code>25 50 44 46 2D</code> (corresponding to <code>%PDF-</code>).</p></li><li><p><code>zip</code> files start with <code>50 4B 03 04</code> (corresponding to <code>PK&#9219;&#9220;</code>).</p></li></ul><p>The code snippet below uses the <code>npm file-type</code> package, which identifies the file type via the magic number, and then proceeds with a security check that can be easily circumvented through the manipulation of the file&#8217;s signature:</p><pre><code>import { fileTypeFromBuffer } from "file-type";

const ALLOWED_TYPES = ["image/jpeg", "image/png"];

const upload = multer({
  ...
});

app.post("/upload", upload.single("file"), async (req, res) =&gt; {
  if (!req.file) {
    res.status(400).json({ message: "No file uploaded" });
    return;
  }

  const { buffer } = req.file;

  // Get the MIME type from buffer
  const fileType = await fileTypeFromBuffer(buffer);

  if (!ALLOWED_TYPES.includes(fileType.mime)) {
    res.status(400).json({ message: "Unexpected file type" });
    return;
  }

  res.send("File uploaded successfully");
});
</code></pre><h3>Bypassing Magic Number Checks</h3><p>Malicious users can easily prepend a valid magic number to malicious files, making them seem legitimate. For instance, adding the <code>%PDF-2.0</code> signature at the start of a <code>webshell</code> file can trick the system into thinking it&#8217;s actually a PDF file. This can actually be the process:</p><ol><li><p>Let&#8217;s start by showing the content of the original <code>webshell.php</code> file:</p></li></ol><pre><code>:~$ cat webshell.php 
&lt;?php system($_GET[&#8221;cmd&#8221;]); ?&gt;</code></pre><ol start="2"><li><p>Then, determine the file type by using the <code>file</code> system command, confirming its original format as PHP:</p></li></ol><pre><code>:~$ file webshell.php
webshell.php: PHP script text, ASCII text</code></pre><ol start="3"><li><p>Proceed to add the appropriate magic number for a PDF file (i.e., <code>%PDF-2.0</code>) at the beginning of the file:</p></li></ol><pre><code>:~$ echo &#8220;%PDF-2.0$(cat webshell.php)&#8221; &gt; webshell.php</code></pre><ol start="4"><li><p>Display the content of the <code>webshell.php</code> file once more to verify the change:</p></li></ol><pre><code>:~$ cat webshell.php
%PDF-2.0&lt;?php system($_GET[&#8221;cmd&#8221;]); ?&gt;</code></pre><ol start="5"><li><p>Show the hex dump of the file to check the initial bytes, ensuring they correspond to <code>25 50 44 46 2D</code> as stated in the <a href="https://en.wikipedia.org/wiki/List_of_file_signatures">list of file signatures</a>:</p></li></ol><pre><code>:~$ xxd webshell.php 
00000000: 2550 4446 2d32 2e30 3c3f 7068 7020 7379  %PDF-2.0&lt;?php sy
00000010: 7374 656d 2824 5f47 4554 5b22 636d 6422  stem($_GET[&#8221;cmd&#8221;
00000020: 5d29 3b20 3f3e 0a                        ]); ?&gt;.</code></pre><ol start="6"><li><p>Determine the file type again using the <code>file</code> command, and notice how it has changed to PDF:</p></li></ol><pre><code>:~$ file webshell.php
webshell.php: PDF document, version 2.0</code></pre><p>As you can see, the outcome is a malicious file that tools that rely on the magic number will report as a PDF.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>File Type Validation via the File Extension</h2><p>In security terms, a file extension is little more than noise. Its role is limited to guiding the operating system&#8212;particularly Windows&#8212;in choosing which application should open the file. Unix-like systems, however, often ignore extensions altogether and instead rely on the magic number to determine the format. Since extensions are not enforced by the OS as protection mechanisms and serve only as usability metadata, they cannot be regarded as a reliable security measure.</p><p>That said, legitimate files are still expected to carry the appropriate extension. This is why extension validation is performed: not as a true security control, but as a superficial filter to quickly reject obvious inconsistencies.</p><p>For instance, the following <code>multer</code> implementation validates extensions by checking for <code>.jpg</code>, <code>.jpeg</code>, or <code>.png</code>, but this simplistic approach is insecure and can be circumvented through a double extension evasion technique:</p><pre><code><code>const express = require(&#8221;express&#8221;);
const multer = require(&#8221;multer&#8221;);

const upload = multer({
  ...
});

const app = express();

app.post(&#8221;/upload&#8221;, upload.single(&#8221;file&#8221;), (req, res) =&gt; {
  if (!req.file) {
    res.status(400).json({ message: &#8220;No file uploaded&#8221; });
    return;
  }

  const { originalname } = req.file;

  if (!originalname.match(/\.(jpg|jpeg|png)/)) {
    res.status(400).json({ message: &#8220;Unexpected file extension&#8221; });
    return;
  }

  res.send(&#8221;File uploaded successfully&#8221;);
});</code></code></pre><h3>How Attackers Bypass File Extension Checks</h3><ul><li><p>Using uppercase letters (e.g., <code>.pHp</code>, <code>.pHP5</code> or <code>.ASP</code>).</p></li><li><p>Adding a valid extension before the execution extension (e.g., <code>image.png.php</code> or <code>image.png.php5</code>).</p></li><li><p>Adding special characters at the end (e.g., <code>file.php%20</code>, <code>file.php%0d%0a</code> or <code>file.php/</code>).</p></li><li><p>Tricking the server-side extension parser by using techniques such as inserting junk data (null bytes) between extensions (e.g., <code>image.php%00.png</code> or <code>image.php\x00.png</code>).</p></li><li><p>Adding another layer of extensions (e.g., <code>image.png.jpg.php</code> or <code>image.php%00.png%00.jpg</code>).</p></li><li><p>Putting the execution extension before the valid extension, which can be useful in case of server misconfigurations (e.g., <code>image.php.png</code>).</p></li><li><p>Using NTFS Alternate Data Stream (ADS) in Windows inserting a colon character <code>:</code> after a forbidden extension and before a permitted one (e.g., <code>image.asp:.jpg</code>).</p></li></ul><h3>Performing Secure File Extension Validation</h3><p>File extensions should only be validated after <a href="https://newsletter.ferranverdes.net/p/file-name-sanitization">file names have been properly sanitized</a>. Keeping this order prevents attackers from hiding tricks within filenames and ensures extension checks are consistently applied. </p><p>Use the following guidelines to keep uploads restricted to safe, authorised extensions:</p><ul><li><p>Decode from URL-encoded format file names prior to validation to prevent bypass techniques like null byte characters (e.g., <code>image.php%00.png</code>).</p></li><li><p>In cases where the web application only accepts a single file type (e.g., <code>.pdf</code>), hardcode the allowed extension when storing the file. If multiple file types are permitted, define an allow-list that restricts file extensions to only those necessary for business needs (e.g., <code>.jpg</code>, <code>.jpeg</code> and <code>.png</code>).</p></li><li><p>Reject files that have missing or multiple extensions, unless explicitly required, to reduce the risk of exploitation.</p></li><li><p>Apply robust filtering when validating to avoid common pitfalls, such as regex patterns that can be bypassed.</p></li></ul><p>The code snippet below secures the file upload feature by properly decoding the file name before validation, applying an allow-list of allowed extensions, and preventing files with multiple or missing extensions:</p><pre><code>const ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png"];

const isAllowedFileExtension = (filename) =&gt; {
  const decodedFilename = decodeURIComponent(filename);
  const lowerCaseFilename = decodedFilename.toLowerCase();
  const lastDotIndex = lowerCaseFilename.lastIndexOf(".");

  if (lastDotIndex === -1) return false; // No extension found
  if (lowerCaseFilename.split(".").length - 1 &gt; 1) return false; // Multiple extension found

  const extension = lowerCaseFilename.slice(lastDotIndex);

  return ALLOWED_EXTENSIONS.includes(extension);
};</code></pre><pre><code>const upload = multer({
  ...
  fileFilter: (req, file, cb) =&gt; {
    if (isAllowedFileExtension(file.originalname)) {
      cb(null, true);
      return;
    }

    const error = new multer.MulterError("LIMIT_UNEXPECTED_FILE", file.fieldname);
    error.message = "Unexpected file extension";
    cb(error);
  }
});</code></pre><h3>File Extension Validation on the Client-Side</h3><p>File extension can also be easily checked on the frontend using the HTML <code>accept</code> attribute, which helps prevent users from sending unexpected files, though it is not reliable for security purposes:</p><pre><code>&lt;input type="file" id="fileInput" accept=".jpg, .jpeg, .png" /&gt;</code></pre><p>To wrap up, the most reliable way to determine a file&#8217;s type is not by checking the <strong>Content-Type</strong> header, the <strong>Magic Number</strong> signature, or the <strong>File Extension</strong>, but by evaluating its actual content with specialised security tools and platforms&#8212;and relying on them to decide whether the file should be accepted or rejected. We&#8217;ll take a closer look at this approach in the upcoming post.</p><h2>Interesting Reads</h2><p>Some interesting articles I read in the past days:</p><ul><li><p><a href="https://github.com/zakirullin/cognitive-load">Cognitive load is what matters</a> by Artem Zakirullin</p></li><li><p><a href="https://tailscale.com/blog/frequent-reauth-security">Frequent reauth doesn&#8217;t make you more secure</a> by Avery Pennarun</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[🍪#2 How To Sanitize A Filename]]></title><description><![CDATA[A developer's guide to practical defenses against unsafe file names in file upload features.]]></description><link>https://newsletter.ferranverdes.net/p/file-name-sanitization</link><guid isPermaLink="false">https://newsletter.ferranverdes.net/p/file-name-sanitization</guid><dc:creator><![CDATA[Ferran]]></dc:creator><pubDate>Thu, 18 Sep 2025 12:18:59 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!kZaA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddcd2478-3128-4509-b4eb-31594fed8aac_1038x400.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!kZaA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddcd2478-3128-4509-b4eb-31594fed8aac_1038x400.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!kZaA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddcd2478-3128-4509-b4eb-31594fed8aac_1038x400.png 424w, https://substackcdn.com/image/fetch/$s_!kZaA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddcd2478-3128-4509-b4eb-31594fed8aac_1038x400.png 848w, https://substackcdn.com/image/fetch/$s_!kZaA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddcd2478-3128-4509-b4eb-31594fed8aac_1038x400.png 1272w, https://substackcdn.com/image/fetch/$s_!kZaA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddcd2478-3128-4509-b4eb-31594fed8aac_1038x400.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!kZaA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddcd2478-3128-4509-b4eb-31594fed8aac_1038x400.png" width="1038" height="400" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ddcd2478-3128-4509-b4eb-31594fed8aac_1038x400.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:400,&quot;width&quot;:1038,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!kZaA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddcd2478-3128-4509-b4eb-31594fed8aac_1038x400.png 424w, https://substackcdn.com/image/fetch/$s_!kZaA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddcd2478-3128-4509-b4eb-31594fed8aac_1038x400.png 848w, https://substackcdn.com/image/fetch/$s_!kZaA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddcd2478-3128-4509-b4eb-31594fed8aac_1038x400.png 1272w, https://substackcdn.com/image/fetch/$s_!kZaA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddcd2478-3128-4509-b4eb-31594fed8aac_1038x400.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>File name sanitization is a crucial process in applications handling file uploads to ensure user-provided names are both safe and compatible with the systems where they will be stored or processed. It involves validating and modifying the original names of uploaded files to prevent security threats or operational failures that could compromise system integrity.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5JbQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F485b2aca-52be-430d-8160-2f7446516bd8_5000x1537.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5JbQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F485b2aca-52be-430d-8160-2f7446516bd8_5000x1537.png 424w, https://substackcdn.com/image/fetch/$s_!5JbQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F485b2aca-52be-430d-8160-2f7446516bd8_5000x1537.png 848w, https://substackcdn.com/image/fetch/$s_!5JbQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F485b2aca-52be-430d-8160-2f7446516bd8_5000x1537.png 1272w, https://substackcdn.com/image/fetch/$s_!5JbQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F485b2aca-52be-430d-8160-2f7446516bd8_5000x1537.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5JbQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F485b2aca-52be-430d-8160-2f7446516bd8_5000x1537.png" width="1456" height="448" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/485b2aca-52be-430d-8160-2f7446516bd8_5000x1537.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:448,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:302388,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.ferranverdes.net/i/173259305?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F485b2aca-52be-430d-8160-2f7446516bd8_5000x1537.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5JbQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F485b2aca-52be-430d-8160-2f7446516bd8_5000x1537.png 424w, https://substackcdn.com/image/fetch/$s_!5JbQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F485b2aca-52be-430d-8160-2f7446516bd8_5000x1537.png 848w, https://substackcdn.com/image/fetch/$s_!5JbQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F485b2aca-52be-430d-8160-2f7446516bd8_5000x1537.png 1272w, https://substackcdn.com/image/fetch/$s_!5JbQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F485b2aca-52be-430d-8160-2f7446516bd8_5000x1537.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>How Attackers Exploit File Names</h2><p>At first glance, a file name looks harmless. But to an attacker, it&#8217;s an opportunity. Because many applications don&#8217;t treat them as untrusted input, they can be abused in clever and dangerous ways.</p><p><strong>Injection payloads</strong></p><p>The risk can come not from the file itself but the way the name is processed. If an application fails to sanitize properly, malicious strings in file names can trigger <strong>existing</strong> vulnerabilities:</p><ul><li><p><code>sleep(20)-- -.jpg</code> &#8594; can lead to SQL injection if the file name is concatenated unsafely into a SQL query without proper escaping or parameterization.</p></li><li><p><code>&lt;svg onload=alert("XSS")&gt;.jpg</code> &#8594; can lead to Cross-Site Scripting (XSS) when the file name is reflected into a web page (HTML, attribute, or JS context) without proper encoding, or when user-supplied SVG content is served/executed in the browser.</p></li><li><p><code>; sleep 20;.jpg</code> &#8594; can lead to OS Command injection if the file name is passed by concatenation into a shell or system API.</p></li></ul><p><strong>Enumeration leaks</strong></p><p>When uploaded files are renamed poorly, attackers can predict or brute-force file names and access other users&#8217; content. For instance, if an application simply appends a counter to duplicates:</p><ul><li><p>First upload: <code>profile.jpg</code> &#8594; stored as <code>profile.jpg</code>.</p></li><li><p>Second upload: <code>profile.jpg</code> &#8594; stored as <code>profile_2.jpg</code>.</p></li><li><p>Third upload: <code>profile.jpg</code> &#8594; stored as <code>profile_3.jpg</code>.</p></li></ul><p>An attacker could guess <code>profile_2.jpg</code>, <code>profile_3.jpg</code>, etc., and retrieve private files from other users.</p><p><strong>Extension tricks and truncation</strong></p><p>Some systems enforce file name length limits, and mishandling them can introduce vulnerabilities. On Linux, where file names are limited to 255 bytes, a crafted example could be:</p><pre><code>aaaa...[very long string]...php.png</code></pre><p>This may bypass a code-based filter yet still be interpreted as PHP if application logic trims or slices names improperly, for example, by truncating the trailing <code>.png</code> to prevent exceeding the length limit.</p><p><strong>Path traversal</strong></p><p>By sneaking in <code>../</code> sequences, attackers can climb outside the intended upload directory and place files where they don&#8217;t belong:</p><pre><code><code>../../../../var/www/html/index.php</code></code></pre><p>This could overwrite the main application file with malicious content.</p><p>As seen, file names may appear harmless, but neglecting to verify them becomes a security risk.</p><p>The code snippet below shows an insecure file upload using <code>multer</code> in an <code>Express</code> app where the user&#8217;s original file name is used without validation:</p><pre><code><code>const multer = require("multer");

const storage = multer.diskStorage({
  filename: (req, file, cb) =&gt; {
    cb(null, file.originalname); // Using file name provided by the user
  }
});</code></code></pre><p>This pattern exposes the application to path traversal, injection attacks, hidden or double-extension files, accidental overwrites and cross-platform naming collisions.</p><h2>Best Secure File Name Practices</h2><p>As a developer, whenever possible, you need to generate unique and random file names (e.g., UUIDs or GUIDs) instead of relying on user-provided names. This solves both operational errors caused by duplicate names and security vulnerabilities tied to user-controlled file names.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!I8pX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fcbb330-f246-43a7-a264-5c58ee6e5f38_547x107.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!I8pX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fcbb330-f246-43a7-a264-5c58ee6e5f38_547x107.png 424w, https://substackcdn.com/image/fetch/$s_!I8pX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fcbb330-f246-43a7-a264-5c58ee6e5f38_547x107.png 848w, https://substackcdn.com/image/fetch/$s_!I8pX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fcbb330-f246-43a7-a264-5c58ee6e5f38_547x107.png 1272w, https://substackcdn.com/image/fetch/$s_!I8pX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fcbb330-f246-43a7-a264-5c58ee6e5f38_547x107.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!I8pX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fcbb330-f246-43a7-a264-5c58ee6e5f38_547x107.png" width="489" height="95.654478976234" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7fcbb330-f246-43a7-a264-5c58ee6e5f38_547x107.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:107,&quot;width&quot;:547,&quot;resizeWidth&quot;:489,&quot;bytes&quot;:17397,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.ferranverdes.net/i/173259305?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fcbb330-f246-43a7-a264-5c58ee6e5f38_547x107.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!I8pX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fcbb330-f246-43a7-a264-5c58ee6e5f38_547x107.png 424w, https://substackcdn.com/image/fetch/$s_!I8pX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fcbb330-f246-43a7-a264-5c58ee6e5f38_547x107.png 848w, https://substackcdn.com/image/fetch/$s_!I8pX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fcbb330-f246-43a7-a264-5c58ee6e5f38_547x107.png 1272w, https://substackcdn.com/image/fetch/$s_!I8pX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7fcbb330-f246-43a7-a264-5c58ee6e5f38_547x107.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>If business requirements make this approach unfeasible, then apply these safeguards:</p><ul><li><p>Enforce a maximum file name length.</p></li><li><p>Restrict allowed characters (e.g., A-Z, a-z, 0-9, -, _, and .).</p></li><li><p>Avoid hidden files and trailing periods or spaces (e.g., .htaccess).</p></li><li><p>Block reserved names in Windows and Linux.</p></li><li><p>Handle file names as case-insensitive.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h3>Use Unique and Random File Names</h3><p>Assigning random and unique names to uploaded files prevents file name collisions, mitigates path traversal attacks, hides original names that could reveal sensitive data, blocks injection attempts, and protects against file enumeration.</p><p>This approach is particularly useful in scenarios where the original file names provided by users do not need to be retained:</p><pre><code>const uuid = require("uuid");

const storage = multer.diskStorage({
  filename: (req, file, cb) =&gt; {
    cb(null, `${uuid.v4()}.pdf`); 
  }
});</code></pre><p>In this illustrative case, the code uses a UUID to generate unique names and enforces the <code>.pdf</code> extension assuming the application is restricted to PDF uploads.</p><h3>When Business Needs Prevent Random Naming</h3><p>Sometimes applications need to preserve user-provided names for usability, compliance, or business logic. In those cases, security should not be sacrificed&#8212;you still can apply strict safeguards to keep file handling safe.</p><h4>Enforce a maximum file name length</h4><p>Different operating systems impose different limits on file name length. Legacy systems like FAT only support the old 8.3 format, while file systems like NTFS allow up to ~255 characters per filename, and ext4 allows up to 255 bytes. Excessive filename length can result in truncation, storage errors, or extension loss that lets files be misread or executed differently. Defining a safe maximum length (for example, 100 characters) prevents both technical complications and potential abuse:</p><pre><code>const MAX_FILENAME_LENGTH = 100;

const decodedFilename = decodeURIComponent(file.originalname);

if (decodedFilename.length &gt; MAX_FILENAME_LENGTH) {
  throw new Error("File name too long");
}</code></pre><h4>Restrict allowed characters</h4><p>Not every character is safe in a file name. Allowing symbols, control characters, or special operators can backfire&#8212;creating risks such as path traversal or injection attacks. A safer approach is to limit file names to a small set of characters, for example: letters (A&#8211;Z, a&#8211;z), numbers (0&#8211;9), dots (.), underscores (_), and hyphens (-). Keeping names simple and predictable helps avoid cross-platform issues and reduces the attack surface significantly:</p><pre><code>// Keep only alphanumerics, dots, underscores, and hyphens
const restrictedFilename = decodedFilename.replace(/[^A-Za-z0-9._-]/g, "");</code></pre><h4>Avoid hidden files and trailing dots or spaces</h4><p>File names that begin with a dot are hidden on UNIX-like systems (for example, <code>.htaccess</code> used by Apache). Attackers can exploit this to disguise malicious uploads or override server configuration. Similarly, names ending with spaces or periods are handled inconsistently across operating systems, sometimes causing access errors or unexpected behavior. Stripping leading dots (unless explicitly allowed) and removing trailing spaces or periods closes this gap and keeps uploaded files predictable and safe:</p><pre><code>// Strip leading dots and trailing dots/spaces
const trimmedFilename = restrictedFilename.replace(/^[.]+|[.\s]+$/g, "");</code></pre><h4>Block reserved names</h4><p>File systems enforce restrictions on certain names that cannot be used for files. In Windows, names such as <code>CON</code>, <code>AUX</code>, <code>PRN</code>, <code>NUL</code>, and <code>COM1&#8211;COM9</code> are reserved, and characters like <code>&lt;</code>, <code>&gt;</code>, <code>:</code>, <code>"</code>, <code>/</code>, <code>\</code>, <code>|</code>, <code>?</code>, and <code>*</code> are not allowed. In Linux and other UNIX-like systems, the null character (<code>\x00</code>) and the forward slash (<code>/</code>) are forbidden in names, while <code>.</code> and <code>..</code> are reserved as directory references. Filtering these values during validation prevents uploaded files from conflicting with the operating system or breaking file system logic:</p><pre><code>const sanitizeFilename = require("sanitize-filename");

// Remove system-reserved filenames
const sanitizedFilename = sanitizeFilename(trimmedFilename);</code></pre><p>This example uses the <code>npm</code> package <a href="https://www.npmjs.com/package/sanitize-filename">sanitize-filename</a>, which automatically strips unsafe characters and reserved names, simplifying validation.</p><h4>Treat file names as case-insensitive</h4><p>On Linux, <code>Report.pdf</code> and <code>report.pdf</code> are two different files. However, on Windows and MacOS, they&#8217;re treated by default as the same file. This mismatch can cause confusion, accidental overwrites, or even unauthorized access. A reliable way to prevent this is to normalize every file name to lowercase (or uppercase) and treat them as case-insensitive. This ensures consistent behavior regardless of where your application runs:</p><pre><code>// Normalize casing for consistency across platforms
const normalizedFilename = sanitizedFilename.toLowerCase();</code></pre><p>It&#8217;s important to keep the order of the operations, otherwise a later step can undo the protections of an earlier one&#8212;for example, restricting characters after stripping leading dots could still leave an unsafe name. By applying checks in the right sequence&#8212;decode, check length, restrict characters, strip dots and spaces, block reserved names, normalize case&#8212;you ensure every safeguard actually holds.</p><h2>Interesting Reads</h2><p>Some interesting articles I read in the past days:</p><ul><li><p><a href="https://newsletter.francofernando.com/p/how-to-become-a-confident-software">How to Become a Confident Software Engineer</a> by <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Franco Fernando&quot;,&quot;id&quot;:47169986,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/58a41b86-1e25-4bd0-a448-138d50731db4_800x800.png&quot;,&quot;uuid&quot;:&quot;bc45d554-bbbc-464b-907b-15a480278759&quot;}" data-component-name="MentionToDOM"></span> </p></li><li><p><a href="https://javarevisited.substack.com/p/rest-api-fundamentals-from-basics">REST API Essentials: What Every Developer Needs to Know</a> by <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;javinpaul&quot;,&quot;id&quot;:16859097,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5663d1cb-2e66-4a0d-8f76-8a3aad3f2382_48x48.png&quot;,&quot;uuid&quot;:&quot;3459e406-2fd4-4ef3-b166-c5f43c84d0b8&quot;}" data-component-name="MentionToDOM"></span> </p></li><li><p><a href="https://horizon3.ai/intelligence/blogs/vulnerable-vs-exploitable-why-understanding-the-difference-matters-to-your-security-posture">Vulnerable vs. Exploitable: Why Understanding the Difference Matters to Your Security Posture</a></p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[🍪#1 The Dangers of Insecure File Uploads]]></title><description><![CDATA[From RCE to data leaks&#8212;the risks behind insecure file handling.]]></description><link>https://newsletter.ferranverdes.net/p/the-dangers-of-insecure-file-uploads</link><guid isPermaLink="false">https://newsletter.ferranverdes.net/p/the-dangers-of-insecure-file-uploads</guid><dc:creator><![CDATA[Ferran]]></dc:creator><pubDate>Tue, 09 Sep 2025 14:19:55 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/08af2eef-4ed4-49ba-b63e-6682723a4bbb_728x295.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As you know, file uploads are a common feature in modern web applications, including profile pictures, documents, reports, media sharing, and many others. But behind their convenience lies one of the most dangerous vulnerabilities: insecure file upload.</p><p>When applications don&#8217;t properly validate the files users submit, such as their name, extension, content, or resource usage, attackers can exploit this to upload malicious files. From there, the consequences can quickly escalate.</p><h2>Why Insecure File Uploads Are So Risky</h2><p>A poorly protected upload function can serve as a gateway to severe attacks:</p><ul><li><p><strong>Remote Code Execution (RCE)</strong>: Malicious scripts uploaded and executed on the server can give attackers full control.</p></li><li><p><strong>Cross-Site Scripting (XSS)</strong>: Uploaded HTML, SVG, or other files containing scripts can be used to hijack user sessions, steal data, or redirect to malicious sites.</p></li><li><p><strong>SQL Injection</strong>: File names used directly in database queries without sanitization can drop tables or exfiltrate data.</p></li><li><p><strong>Command Injection</strong>: File names passed to system commands can execute arbitrary code.</p></li><li><p><strong>Parser Flaws</strong>: Vulnerabilities in file parsers (PDF, XML, media) can be abused for crashes or code execution.</p></li><li><p><strong>Path Traversal</strong>: Manipulated filenames (../) can traverse directories, exposing or altering sensitive files, enabling defacement and integrity loss.</p></li><li><p><strong>Data Leakage</strong>: Metadata or hidden content inside files can expose sensitive information.</p></li><li><p><strong>Malware Delivery</strong>: Files can carry different types of malware targeting servers or end users.</p></li><li><p><strong>Phishing</strong>: Disguised uploads (e.g., invoices, HR docs) can trick employees into revealing sensitive information.</p></li><li><p><strong>Denial of Service (DoS)</strong>: Oversized or excessive uploads can overwhelm server resources, disrupt network performance, or limit storage capacity, causing service downtime.</p></li></ul><h2>Components Attackers Exploit in File Uploads</h2><p>When it comes to insecure uploads, attackers don&#8217;t just rely on a single trick, but every part of a file can become an attack vector if left unchecked.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pt5h!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77ec22f3-af68-41ab-befd-9024a7a827a4_639x221.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pt5h!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77ec22f3-af68-41ab-befd-9024a7a827a4_639x221.png 424w, https://substackcdn.com/image/fetch/$s_!pt5h!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77ec22f3-af68-41ab-befd-9024a7a827a4_639x221.png 848w, https://substackcdn.com/image/fetch/$s_!pt5h!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77ec22f3-af68-41ab-befd-9024a7a827a4_639x221.png 1272w, https://substackcdn.com/image/fetch/$s_!pt5h!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77ec22f3-af68-41ab-befd-9024a7a827a4_639x221.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pt5h!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77ec22f3-af68-41ab-befd-9024a7a827a4_639x221.png" width="639" height="221" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/77ec22f3-af68-41ab-befd-9024a7a827a4_639x221.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:221,&quot;width&quot;:639,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:232971,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.ferranverdes.net/i/173083529?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77ec22f3-af68-41ab-befd-9024a7a827a4_639x221.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!pt5h!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77ec22f3-af68-41ab-befd-9024a7a827a4_639x221.png 424w, https://substackcdn.com/image/fetch/$s_!pt5h!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77ec22f3-af68-41ab-befd-9024a7a827a4_639x221.png 848w, https://substackcdn.com/image/fetch/$s_!pt5h!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77ec22f3-af68-41ab-befd-9024a7a827a4_639x221.png 1272w, https://substackcdn.com/image/fetch/$s_!pt5h!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77ec22f3-af68-41ab-befd-9024a7a827a4_639x221.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p><strong>File Name</strong></p><p>Manipulated names can include path traversal sequences (../) to escape directories, overwrite sensitive files, inject malicious SQL payloads to alter queries and exfiltrate data, or even trigger commands when passed to system utilities.</p><p><strong>File Extension</strong></p><p>Executable files (.exe, .elf) can run malware or perform unwanted actions, while source files (.php, .js, .py) may be interpreted by the server, leading to arbitrary code execution. Even compressed files (.zip, .tar) can smuggle multiple malicious payloads.</p><p><strong>File Content</strong></p><p>PDFs, office docs, and media files can hide embedded scripts, obfuscated payloads, or parser exploits. Some uploads are crafted for phishing&#8212;appearing as invoices or reports to lure victims into revealing data.</p><p><strong>File Limits</strong></p><p>Oversized or repeated uploads can consume CPU, RAM, bandwidth, or storage, leading to resource exhaustion and potential Denial of Service (DoS).</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.ferranverdes.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading <em>The Secure Cookie</em>! To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>How to Secure File Uploads</h2><p>Because there&#8217;s no single silver bullet, securing file uploads requires a <strong>defense-in-depth strategy</strong>&#8212;multiple layers of validation, sanitization, and controls. The goal is simple: no file should ever be processed or stored unless it has passed every security check.</p><h3>Key Best Practices</h3><ul><li><p><strong>Start with general validation</strong>: Ensure that any existing general input validation process is performed prior to other validation steps.</p></li><li><p><strong>Sanitize file names</strong>: Generate random names (e.g., UUIDs) for storing or enforce strict allow-lists for characters and length.</p></li><li><p><strong>Control extensions</strong>: Only allow business-required extensions; block absent and redundant extensions.</p></li><li><p><strong>Check actual content</strong>: Verify file type against its data and scan with antivirus or sandbox tools.</p></li><li><p><strong>Set limits</strong>: Enforce maximum file size and prevent excessive or repeated uploads.</p></li><li><p><strong>Harden storage</strong>: Store files on a dedicated service or at least outside the webroot, enforce least-privilege permissions, and avoid execute rights.</p></li><li><p><strong>Require authentication</strong>: Restrict who can upload and who can access uploaded files.</p></li><li><p><strong>Use trusted frameworks</strong>: Leverage mature libraries for preprocessing instead of reinventing validation.</p></li><li><p><strong>Protect against CSRF</strong>: Ensure upload endpoints are safeguarded against Cross-Site Request Forgery (CSRF).</p></li><li><p><strong>Stay updated</strong>: Keep dependencies and parsing libraries patched and securely configured.</p></li></ul><p>In the upcoming posts, I&#8217;ll break down each of these practices in detail, showing how attackers exploit weak points and how to implement the right defenses.</p><h2>Reading Picks</h2><p>Here are a few articles I found valuable in recent weeks:</p><ul><li><p><a href="https://newsletter.francofernando.com/p/apis-versioning?r=34xvu1&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=false">APIs Versioning</a> by <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Franco Fernando&quot;,&quot;id&quot;:47169986,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/58a41b86-1e25-4bd0-a448-138d50731db4_800x800.png&quot;,&quot;uuid&quot;:&quot;21d4b7c8-a300-4b06-ba18-84c3d697b75d&quot;}" data-component-name="MentionToDOM"></span></p></li><li><p><a href="https://flatt.tech/research/posts/why-xss-persists-in-this-frameworks-era/">Why XSS Persists in This Frameworks Era?</a> by Canalun</p></li><li><p><a href="https://www.outputtheory.com/p/the-simple-habit-that-lowers-stress?r=34xvu1&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=false">The Simple Habit That Lowers Stress, Boosts Energy, and Improves Health</a> by <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Tobias Winkler&quot;,&quot;id&quot;:355775399,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/75486779-362a-4295-8033-1ef8d9db8dba_808x808.jpeg&quot;,&quot;uuid&quot;:&quot;d08b1cdc-72e6-46db-9eb7-d9828d320d0e&quot;}" data-component-name="MentionToDOM"></span></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Hello from The Secure Cookie]]></title><description><![CDATA[An introduction to me and to this newsletter. And why secure coding is more than just following AI hints.]]></description><link>https://newsletter.ferranverdes.net/p/hello-from-my-secure-coding-notes</link><guid isPermaLink="false">https://newsletter.ferranverdes.net/p/hello-from-my-secure-coding-notes</guid><dc:creator><![CDATA[Ferran]]></dc:creator><pubDate>Fri, 05 Sep 2025 15:50:13 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5fff5621-73b2-4c90-bd75-3561d38b0e1d_560x400.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-2UA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38a582ce-87bd-4af7-ba78-e0a051fb8798_510x451.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-2UA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38a582ce-87bd-4af7-ba78-e0a051fb8798_510x451.png 424w, https://substackcdn.com/image/fetch/$s_!-2UA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38a582ce-87bd-4af7-ba78-e0a051fb8798_510x451.png 848w, https://substackcdn.com/image/fetch/$s_!-2UA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38a582ce-87bd-4af7-ba78-e0a051fb8798_510x451.png 1272w, https://substackcdn.com/image/fetch/$s_!-2UA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38a582ce-87bd-4af7-ba78-e0a051fb8798_510x451.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-2UA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38a582ce-87bd-4af7-ba78-e0a051fb8798_510x451.png" width="280" height="247.6078431372549" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/38a582ce-87bd-4af7-ba78-e0a051fb8798_510x451.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:451,&quot;width&quot;:510,&quot;resizeWidth&quot;:280,&quot;bytes&quot;:29324,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://ferranverdes.substack.com/i/172881072?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38a582ce-87bd-4af7-ba78-e0a051fb8798_510x451.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-2UA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38a582ce-87bd-4af7-ba78-e0a051fb8798_510x451.png 424w, https://substackcdn.com/image/fetch/$s_!-2UA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38a582ce-87bd-4af7-ba78-e0a051fb8798_510x451.png 848w, https://substackcdn.com/image/fetch/$s_!-2UA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38a582ce-87bd-4af7-ba78-e0a051fb8798_510x451.png 1272w, https://substackcdn.com/image/fetch/$s_!-2UA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38a582ce-87bd-4af7-ba78-e0a051fb8798_510x451.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Curiosity, learning, and hands-on testing are what brought us here today, to this online exchange between writer and reader. Knowledge has always fueled human progress, and it continues to shape the way we understand and build technology.</p><p>However, AI is now reshaping nearly everything we do, especially how we develop applications. Developers used to spend hours digging, testing, and figuring things out. Now we often just ask. That makes us faster and more productive, but when it comes to complex areas like security, things aren&#8217;t that simple. Security influences everything, from system performance and application usability to data integrity and user privacy.</p><p>The truth is, you can&#8217;t build secure systems without really understanding security. If you don&#8217;t know the threats, you can&#8217;t pick the right defenses. AI can give useful hints, but without the right context, you might not apply them correctly, and that&#8217;s when trouble happens.</p><p>These days, people assume applications need to be secure by default. Teams expect developers to deliver features quickly, meet business requirements, and avoid security gaps. All at once.</p><p>This is precisely why knowledge remains such a powerful advantage. AI is great, but genuine understanding gives you perspective. It helps you learn faster, solve harder problems, and work smarter.</p><p>For me, building secure systems is not just about adhering to best practices or integrating AI-driven advice. It is about being prepared for challenging scenarios, deeply understanding the technology, and enjoying the learning process itself.</p><p>That&#8217;s what motivates me to write this newsletter: to share what I&#8217;ve researched, tested, and learned in terms of security, hopefully in a way that makes life a little easier for all engineers.</p><p>I hope you find it useful.</p><p>Ferran</p>]]></content:encoded></item></channel></rss>