<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alessandro</title>
    <description>The latest articles on DEV Community by Alessandro (@pazent).</description>
    <link>https://dev.to/pazent</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4077522%2F1b5eb799-d0b4-4ae4-b0ca-79f185adfca2.png</url>
      <title>DEV Community: Alessandro</title>
      <link>https://dev.to/pazent</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pazent"/>
    <language>en</language>
    <item>
      <title>The BOLA bug was fixed. What proves it stays fixed?</title>
      <dc:creator>Alessandro</dc:creator>
      <pubDate>Mon, 17 Aug 2026 21:31:43 +0000</pubDate>
      <link>https://dev.to/pazent/the-bola-bug-was-fixed-what-proves-it-stays-fixed-410a</link>
      <guid>https://dev.to/pazent/the-bola-bug-was-fixed-what-proves-it-stays-fixed-410a</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7dplf39pzogmejyw6vvg.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7dplf39pzogmejyw6vvg.jpg" alt="ExploitSpec — turn proven exploits into permanent regression tests" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A pentest report can prove that user B can read user A's object. The team fixes&lt;br&gt;
the authorization check, closes the ticket, and moves on.&lt;/p&gt;

&lt;p&gt;Six months later, a refactor removes that check.&lt;/p&gt;

&lt;p&gt;The original report still exists, but the proof is no longer running anywhere.&lt;br&gt;
An ordinary happy-path API test usually cannot distinguish “the owner can read&lt;br&gt;
the object” from “every authenticated user can read the object.” A scanner may&lt;br&gt;
rediscover the bug eventually, but that is not the same as preserving the exact&lt;br&gt;
security property the team already learned.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/pazent/exploitspec" rel="noopener noreferrer"&gt;ExploitSpec&lt;/a&gt; around a narrow&lt;br&gt;
idea: once a human has proven an HTTP exploit, turn that proof into a small,&lt;br&gt;
reviewable regression test that lives beside the code.&lt;/p&gt;

&lt;p&gt;It is deliberately &lt;strong&gt;not a scanner&lt;/strong&gt;. It does not decide whether an endpoint is&lt;br&gt;
vulnerable. The human supplies the meaningful invariant; the runner makes that&lt;br&gt;
invariant deterministic enough for CI.&lt;/p&gt;
&lt;h2&gt;
  
  
  The invariant, not just the request
&lt;/h2&gt;

&lt;p&gt;Consider a BOLA/IDOR finding:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;an owner creates a private invoice;&lt;/li&gt;
&lt;li&gt;the test captures the runtime invoice ID and payment reference;&lt;/li&gt;
&lt;li&gt;another authenticated tenant requests that exact invoice;&lt;/li&gt;
&lt;li&gt;the response must be &lt;code&gt;403&lt;/code&gt; or &lt;code&gt;404&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the body must not contain the captured payment reference or sensitive JSON
fields.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important part is the relationship between two actors and a dynamic object,&lt;br&gt;
not a hard-coded URL copied from a proxy history.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SEC-BOLA-001&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cross-tenant invoice access is denied&lt;/span&gt;

&lt;span class="na"&gt;actors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${OWNER_TOKEN}"&lt;/span&gt;
  &lt;span class="na"&gt;attacker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${ATTACKER_TOKEN}"&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Owner creates a private invoice&lt;/span&gt;
    &lt;span class="na"&gt;actor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;owner&lt;/span&gt;
    &lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POST&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/api/invoices&lt;/span&gt;
      &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;customer_email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alice@tenant-a.example&lt;/span&gt;
        &lt;span class="na"&gt;amount_cents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4200&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;201&lt;/span&gt;
    &lt;span class="na"&gt;capture&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;invoice_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;json_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$.id&lt;/span&gt;
      &lt;span class="na"&gt;payment_reference&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;json_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$.payment_reference&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Another tenant cannot read it&lt;/span&gt;
    &lt;span class="na"&gt;actor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;attacker&lt;/span&gt;
    &lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/invoices/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;invoice_id&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;403&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;404&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;not_contains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;payment_reference&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;not_exists&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$.customer_email&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$.payment_reference&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each actor receives an independent cookie jar and header set. Values captured by&lt;br&gt;
one step can be used by later steps, so a test does not depend on fixture IDs.&lt;/p&gt;
&lt;h2&gt;
  
  
  RED, GREEN, and STABLE
&lt;/h2&gt;

&lt;p&gt;A test that passes against the fixed version is not yet trustworthy. It could&lt;br&gt;
also pass against the vulnerable version, or succeed only because a fixture is&lt;br&gt;
missing.&lt;/p&gt;

&lt;p&gt;ExploitSpec therefore calibrates the same invariant against two known states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;exploitspec calibrate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vulnerable-url&lt;/span&gt; http://127.0.0.1:18080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--fixed-url&lt;/span&gt; http://127.0.0.1:18081 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--runs&lt;/span&gt; 3 &lt;span class="se"&gt;\&lt;/span&gt;
  finding.exploit.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ExploitSpec calibration  SEC-BOLA-001
  + RED    invariant fails on the vulnerable baseline
  + GREEN  invariant passes after the fix
  + STABLE fix passes 3/3 runs

Calibration accepted. This spec can now guard the fix in CI.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, RED does not mean “critical severity.” It means the invariant correctly&lt;br&gt;
detects the known-vulnerable state. GREEN proves the fixed state satisfies the&lt;br&gt;
invariant. STABLE repeats the fixed-state check to expose flaky tests before the&lt;br&gt;
team trusts them.&lt;/p&gt;

&lt;p&gt;You can run the complete local demonstration with only Go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/pazent/exploitspec.git
&lt;span class="nb"&gt;cd &lt;/span&gt;exploitspec
make demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a&lt;br&gt;
&lt;a href="https://github.com/pazent/exploitspec-demo" rel="noopener noreferrer"&gt;minimal consumer repository&lt;/a&gt; that&lt;br&gt;
runs the released GitHub Action and publishes JUnit output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why plain YAML?
&lt;/h2&gt;

&lt;p&gt;The format is intentionally boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the security invariant can be reviewed in a pull request;&lt;/li&gt;
&lt;li&gt;the test stays with the application instead of a SaaS account;&lt;/li&gt;
&lt;li&gt;captured evidence and assertions have explicit names;&lt;/li&gt;
&lt;li&gt;deterministic exit codes and JUnit/JSON reports fit existing CI;&lt;/li&gt;
&lt;li&gt;there is no model call in the execution path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The runner is local-first, has no account or telemetry, and is Apache-2.0&lt;br&gt;
licensed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Importing evidence without committing secrets
&lt;/h2&gt;

&lt;p&gt;ExploitSpec can turn one supported cURL command into a starter file, but treats&lt;br&gt;
the input as hostile data. It never invokes a shell or sends the request.&lt;/p&gt;

&lt;p&gt;The importer conservatively replaces query and form values, JSON string leaves,&lt;br&gt;
and non-allowlisted header values with required environment variables. It&lt;br&gt;
rejects malformed URLs, shell control syntax, unsupported opaque bodies, and an&lt;br&gt;
existing output path.&lt;/p&gt;

&lt;p&gt;That is intentionally noisier than guessing which value “looks like a secret.”&lt;br&gt;
The generated file is a starting point for a human review, not a magic finding&lt;br&gt;
converter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current limits
&lt;/h2&gt;

&lt;p&gt;The project is pre-1.0 and deliberately narrow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it executes HTTP invariants; it does not discover vulnerabilities;&lt;/li&gt;
&lt;li&gt;authors must control cleanup for mutating requests;&lt;/li&gt;
&lt;li&gt;explicitly allowed hostnames are not DNS-pinned;&lt;/li&gt;
&lt;li&gt;proxy behavior follows Go's environment configuration and still needs a wider
compatibility matrix;&lt;/li&gt;
&lt;li&gt;imported evidence still requires review before commit or execution;&lt;/li&gt;
&lt;li&gt;a regression test is only as strong as its assertions and test data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those limits are documented because a security tool should make its trust&lt;br&gt;
boundary inspectable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question I want help answering
&lt;/h2&gt;

&lt;p&gt;Which confirmed AppSec finding is hardest to encode as a deterministic HTTP&lt;br&gt;
regression test: authorization boundaries, business-logic races, signed&lt;br&gt;
requests, or something else?&lt;/p&gt;

&lt;p&gt;The repository, runnable demo, specification, and open roadmap are at&lt;br&gt;
&lt;a href="https://github.com/pazent/exploitspec" rel="noopener noreferrer"&gt;github.com/pazent/exploitspec&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I created and maintain ExploitSpec. I used an AI coding assistant&lt;br&gt;
to help edit this article; every command and technical claim above was checked&lt;br&gt;
against the public repository.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devsecops</category>
      <category>opensource</category>
      <category>go</category>
    </item>
  </channel>
</rss>
