<?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: Hassan Balbakie</title>
    <description>The latest articles on DEV Community by Hassan Balbakie (@balbaks).</description>
    <link>https://dev.to/balbaks</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%2F4043631%2Fe3e25d44-9868-4684-b32a-df9732a620d4.png</url>
      <title>DEV Community: Hassan Balbakie</title>
      <link>https://dev.to/balbaks</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/balbaks"/>
    <language>en</language>
    <item>
      <title>I benchmarked my scanner against Semgrep. It lost — sort of.</title>
      <dc:creator>Hassan Balbakie</dc:creator>
      <pubDate>Sun, 13 Sep 2026 13:49:35 +0000</pubDate>
      <link>https://dev.to/balbaks/i-benchmarked-my-scanner-against-semgrep-it-lost-sort-of-542j</link>
      <guid>https://dev.to/balbaks/i-benchmarked-my-scanner-against-semgrep-it-lost-sort-of-542j</guid>
      <description>&lt;p&gt;A few weeks ago I ran inlet, a small static SQLi scanner I built, against 15 real Python packages — 5 with documented CVEs, 10 without — and reported the result plainly: 0 clean hits on the 5 CVEs, 1 partial. The obvious next question was whether an established tool would've done better on the same corpus. So I ran it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same 15 packages, same versions, same CVE fix-commits used as ground truth. Semgrep, using its current public Python security rulesets. No changes to either tool — this was a reference run, not a tuning exercise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result that actually surprised me&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Semgrep missed the exact same 4 CVEs inlet missed. Not similar ones — the same four, and for the same reason: the vulnerable line is reached through a framework abstraction (a hook method, a query-builder helper) that never literally calls anything either tool's rules recognize as a SQL-execution sink. Neither tool's idiom coverage extends that far.&lt;/p&gt;

&lt;p&gt;This is a more interesting result than either "Semgrep obviously wins" or "inlet holds its own" would have been, because of what it rules out. Before this benchmark, inlet's 4/5 miss rate had an easy, dismissive explanation available: small idiom list, one week of work, of course it misses things a mature community-maintained tool wouldn't. This result removes that explanation. If years of accumulated Semgrep rules also miss these exact four, for the exact same mechanism, the ceiling isn't "inlet needs more rules" — it's a real limit on what name-based static matching can see at all, regardless of how many names you add to the list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where they actually differed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Semgrep caught one CVE (Archery's) that inlet only partially got — the vulnerable f-string assignment sat inside a try: block, a name-resolution gap in inlet's local-scope tracing that Semgrep's rule engine handled better.&lt;/p&gt;

&lt;p&gt;On the 10 non-CVE packages, inlet flagged real risk shapes in a few (records, dataset, SQLModel, Flask-SQLAlchemy) that Semgrep's shape-based rules structurally don't attempt — largely because inlet has an explicit uncertain verdict for "I can't resolve this argument," while a lot of static tools either flag confidently or stay silent, with nothing in between.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means, stated plainly&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Neither tool dominates the other on this corpus. That's the honest outcome, and it's more useful than a clean win would have been — it turns inlet's original 0/5 from "here's a limitation of my implementation" into "here's evidence about a limitation of the approach itself," which is a meaningfully stronger claim, and one I couldn't make before running this comparison.&lt;/p&gt;

&lt;p&gt;Full results, per-package notes, and the fairness caveats (Semgrep's multi-year community maintenance vs. inlet's one-week solo build, judgment calls in rule selection) are in EVALUATION.md:&lt;/p&gt;

&lt;p&gt;github.com/balbaks/inlet&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>python</category>
      <category>security</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>I ran my scanner against 5 real CVEs. It missed 4. Then I reverted my own fix.</title>
      <dc:creator>Hassan Balbakie</dc:creator>
      <pubDate>Sat, 12 Sep 2026 21:51:23 +0000</pubDate>
      <link>https://dev.to/balbaks/i-ran-my-scanner-against-5-real-cves-it-missed-4-then-i-reverted-my-own-fix-4dhk</link>
      <guid>https://dev.to/balbaks/i-ran-my-scanner-against-5-real-cves-it-missed-4-then-i-reverted-my-own-fix-4dhk</guid>
      <description>&lt;p&gt;&lt;strong&gt;Why this post is different from the last one&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The last write-up in this series announced four tools. This one is about what happened when I stopped writing tests for my own tools and started checking one of them against reality — and about the fix I built, tested, shipped, and then took back out, because it was wrong in a way that only showed up once I looked past the headline number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup: inlet, and the claim it hadn't actually tested&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;inlet&lt;/strong&gt; is a static scanner: point it at a Python codebase, it finds every call site that looks like SQL execution — raw DB API calls, Django's .raw()/.extra(), SQLAlchemy's text() — and classifies each as parameterized, concatenated, or uncertain. Every claim in its original README was backed by 7 hand-written fixtures, each proving one specific classification rule.&lt;/p&gt;

&lt;p&gt;That's real, but it's not evidence of anything beyond "the mechanism works on cases designed to exercise it." So I built a real-world evaluation: 15 PyPI packages, split into two groups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Group A (5 packages):&lt;/strong&gt; each with a documented, independently verified historical SQL-injection CVE, with the fix commit or advisory located ahead of time so I could check inlet's output against ground truth, not against inlet's own opinion of itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Group B (10 packages):&lt;/strong&gt; popular, no known SQLi history — a noise-floor check on how often inlet flags something that isn't actually a risk.&lt;/p&gt;

&lt;p&gt;~772K lines of real code, scanned unmodified, in about 10.6 seconds combined.&lt;/p&gt;

&lt;p&gt;The result: 0 for 5&lt;/p&gt;

&lt;p&gt;Django (CVE-2022-28346), Apache Superset (CVE-2023-49736), Tortoise ORM (CVE-2020-11010), and Airflow's common-sql provider (CVE-2025-30473) were all complete misses. Archery's CVE-2023-30556 was a partial — the exact vulnerable line showed up in inlet's output, but classified uncertain instead of concatenated, because the f-string assignment sat inside a try: block, a name-resolution gap beyond the documented scope wall.&lt;/p&gt;

&lt;p&gt;The pattern behind all four full misses was identical: the vulnerable code never literally calls something named .execute(), .raw(), .extra(), or text(). It goes through a framework's own abstraction — hook.get_records(), field.like(), a plain helper function — that eventually reaches SQL execution, several calls away from any name inlet recognizes.&lt;/p&gt;

&lt;p&gt;I put that as the headline of EVALUATION.md, not a footnote. A 0/5 result, reported plainly, is worth more than a clean-looking demo — it's the first piece of evidence in this whole project that came from checking against something I didn't design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The smaller, real bug the same evaluation found&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Group B surfaced something fixable: one package's uncertain findings were 67% false positives, from a name collision. peewee's own query builder has an .execute(database) method — same method name as a real DB cursor's .execute(sql), completely different meaning. inlet's name-only matching couldn't tell them apart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix that worked, and then didn't&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I built a positive-evidence rule: only treat an .execute()-shaped call as a real DB-idiom candidate if there's actual evidence for it — either the argument is string-shaped, or the receiver chain shows a .cursor() call or a conventional cursor/connection name. Otherwise, exclude it.&lt;/p&gt;

&lt;p&gt;It worked, exactly as intended, on peewee: 33 uncertain findings down to 9, a clean diff confirming all 24 removed were the exact false-positive shape, zero true positives lost.&lt;/p&gt;

&lt;p&gt;Then I re-ran the other 9 Group B packages, and found the same rule had silently dropped 94 real database call sites — Django's SchemaEditor.execute(), SQLAlchemy's own Engine/Session internals, a dataset helper, SQLModel's super().execute(). All real DB calls, lost for one reason: their receiver was named something generic like self, which the new rule couldn't distinguish from peewee's unrelated Query.execute().&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I reverted it instead of tuning it further&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's a version of this where I keep iterating the heuristic, trying to find a cleverer rule that keeps the peewee win without the 94-finding cost. I didn't do that, because the actual finding underneath both results is more important than either number:&lt;/p&gt;

&lt;p&gt;When the argument isn't string-shaped and the receiver name is generic, there is no way to tell a real DB wrapper from an unrelated same-named method using local syntax alone. self.execute(x) is genuinely, irreducibly ambiguous from where inlet sits. That's not a heuristic to keep tuning — it's the same category of hard limit as the tool's existing cross-function-scope wall.&lt;/p&gt;

&lt;p&gt;And there's an asymmetry that matters more than either number: a finding in uncertain is recoverable — a human can look at it and dismiss it. A finding that's silently excluded is not recoverable — it never existed for anyone to see. Trading visible noise for confident silence is a strictly worse failure mode, even when the summary metric (fewer uncertain findings!) looks like an improvement.&lt;/p&gt;

&lt;p&gt;So I reverted it. Every .execute()-shaped candidate goes back to being reported, at whatever verdict the classifier can actually support — no silent exclusion, ever. The receiver-evidence detection code is still there, inert, available as a future upgrade-only signal (never a removal signal) if a principled way to use it that way ever turns up.&lt;/p&gt;

&lt;p&gt;I wrote the whole thing up as its own section in EVALUATION.md — what broke, why it was reverted, and the actual finding — because a failed attempt with an honest postmortem is a better artifact than either the original bug or a fix that quietly traded one failure mode for a worse one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then, a fifth tool: escrow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Separately, I built escrow, which vets a Python package before a real pip install by actually installing and importing it in a sandbox first — built directly on two earlier tools in this series: husk (the hardened sandbox) and witness (the audit-hook behavior reporter). This exists because of slopsquatting: LLMs hallucinate plausible-but-nonexistent package names at meaningful rates, attackers register those exact names, and the next pip install executes whatever they put there — this is already a real, documented attack pattern, not a hypothetical.&lt;/p&gt;

&lt;p&gt;Building it surfaced a real limitation in witness's own technique: witness observes behavior by prepending an audit-hook preamble to a script running in one interpreter process. That can't see into pip's own build-backend subprocess — exactly where install-time (setup.py) attacks actually run. escrow's hook ships instead as a real sitecustomize.py, auto-loaded by Python's own site module in every subprocess pip spawns, not just the top-level driver. Found and fixed empirically, including discovering that pip install silently swallows successful build-step subprocess output unless run with --verbose — which would have hidden a caught-and-ignored malicious write from the very report meant to catch it.&lt;/p&gt;

&lt;p&gt;escrow keeps its own honest limit stated up front: installing a real package requires network access, so anything malicious that completes fast enough during that window can be detected and reported, but not prevented in real time. That's not a gap to be engineered around in v0.1.0 — it's a fundamental property of vetting something that needs network access to install at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The actual pattern across all five tools now&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;secfix refuses to say "fixed" without a fresh trace. husk backs every hardening claim with an adversarial test. witness turns its own blind spot into a loud signal instead of a silent gap. inlet measured itself against real CVEs, got a bad number, and reported it as the headline. And when a fix improved that number by making the tool quietly worse in a different way, it went back out — documented, not buried.&lt;/p&gt;

&lt;p&gt;That's the thing I'm actually trying to build a track record of. Not five clever tools. Five tools that keep finding their own mistakes before anyone else has to.&lt;/p&gt;

&lt;p&gt;Repos: github.com/balbaks/secfix · github.com/balbaks/husk · github.com/balbaks/witness · github.com/balbaks/inlet · github.com/balbaks/escrow&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>testing</category>
    </item>
    <item>
      <title>Four small security tools in a month, and the wall each one hit on purpose</title>
      <dc:creator>Hassan Balbakie</dc:creator>
      <pubDate>Fri, 11 Sep 2026 14:17:36 +0000</pubDate>
      <link>https://dev.to/balbaks/four-small-security-tools-in-a-month-and-the-wall-each-one-hit-on-purpose-3a70</link>
      <guid>https://dev.to/balbaks/four-small-security-tools-in-a-month-and-the-wall-each-one-hit-on-purpose-3a70</guid>
      <description>&lt;p&gt;&lt;strong&gt;The shape of this post&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This isn't a "look what I built" post. It's closer to a lab notebook — four small tools, shipped in sequence, each one built on the last, and each one deliberately documenting where it stops working rather than papering over it. If there's a thesis, it's this: a security tool's README is only as trustworthy as the test backing its weakest claim. So I tried to make every claim in every README point at a specific test that proves it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. secfix — the one that started it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The idea: most scanner findings are unverified guesses. Semgrep (or whatever) flags a line, and it's on you to figure out if it's actually exploitable. secfix closes that gap by executing the flagged code — building a pytest harness with a tainted sentinel value, running it in a locked-down Docker sandbox, and inspecting the execution trace (not the source) to decide confirmed / not_reproduced / uncertain. For confirmed findings, it generates a patch and re-runs the same harness on a fresh trace — it only calls something validated if that fresh trace proves the fix.&lt;/p&gt;

&lt;p&gt;The single most valuable result in the whole project came from testing secfix against a real app: pygoat, a deliberately vulnerable Django app, scored 0 out of 84 findings reaching any verdict on the first pass.&lt;/p&gt;

&lt;p&gt;That's not a failure, that's data. It mapped a real gradient of walls:&lt;/p&gt;

&lt;p&gt;. DB-idiom detection missing common patterns like connection.cursor()&lt;br&gt;
. Python/Django version mismatches breaking the sandbox&lt;br&gt;
. Real vulnerabilities living in Django views that take request, not scalar parameters&lt;br&gt;
. The hard wall: views doing DB lookups and permission checks before the vulnerable line, needing a seeded, migrated database and the exact right row to exist&lt;/p&gt;

&lt;p&gt;The first three generalize with bounded engineering effort. The fourth — inferring a view's full data dependencies automatically — is genuinely team-scale work, the kind companies like Snyk spend years on. Getting one real Django view SQLi to a confirmed verdict required hand-supplying that knowledge. Worth doing once, to know exactly where the line is. Not worth faking past.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. husk — pulling the sandbox out on its own&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Buried inside secfix was a genuinely reusable piece: the locked-down Docker sandbox used to run flagged code safely. So I extracted it, generalized it, and gave it one job — detonate() untrusted Python, return stdout/stderr/exit code/timing, prove it's actually isolated.&lt;/p&gt;

&lt;p&gt;The thing I wanted to fix here specifically: most sandbox READMEs list hardening flags (--network none, read-only rootfs, etc.) and never test whether an attacker running inside the container can actually violate them. So husk ships an adversarial test suite — scripts that actively try to open sockets, escape the filesystem, fork-bomb, and escalate privileges — and every claim in the README is tied to the specific test that proves it, checking the specific failure mode (a real Network is unreachable error, not just a nonzero exit code that could mean anything).&lt;/p&gt;

&lt;p&gt;One design choice worth flagging: code is streamed into the container over stdin, never bind-mounted as a file. That removes the usual "which host paths are exposed" question entirely, rather than answering it carefully.&lt;/p&gt;

&lt;p&gt;And stated above the fold, not hidden: husk shares the host kernel. It's Docker, not a hypervisor boundary — not a substitute for gVisor, Firecracker, or Kata against genuinely hostile code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. witness — from "did it work" to "what did it try"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;husk tells you an attempt failed. It doesn't tell you what was attempted. witness closes that gap using Python's built-in audit-hook system (sys.addaudithook, PEP 578), which fires on operations like socket.connect, open, and subprocess.Popen before the sandbox's own isolation blocks them.&lt;/p&gt;

&lt;p&gt;The mechanism reuses a trick from secfix: a unique per-run marker gets prepended to stderr output on every observed event, so the runner can cleanly split "structured behavior report" from "the script's actual stderr" afterward. Same sentinel-taint idea, different job.&lt;/p&gt;

&lt;p&gt;Two walls came out of this branch, and only one was planned:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The intended wall:&lt;/strong&gt; audit hooks only see Python-level API calls. Code that drops to ctypes and calls a raw syscall directly goes dark — the report can't see past that ctypes.dlopen call. So witness treats that call itself as a loud, explicit signal ("opaque escape hatch used, subsequent behavior not observable") instead of silently missing whatever happens next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The wall I didn't expect:&lt;/strong&gt; while wiring up the privilege-escalation category, os.setuid turned out to fire no audit event in CPython at all — not a bug in witness, a real fact about the interpreter, confirmed against CPython's own audit-events documentation. Rather than quietly drop that code path, I wrote a dedicated fixture and test proving it, and documented it as a second wall right next to the ctypes one. That's the kind of thing you only find by actually trying to build the feature and having it not work the way you assumed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. inlet — closing the loop on secfix's own Wall A&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;secfix's walls doc named a real, generalizable gap: DB-idiom detection missed patterns like connection.cursor(). inlet is that fix, built out as its own standalone static scanner: walk a Python codebase, find every call site that looks like SQL execution (raw DB API, Django .raw() / .extra(), SQLAlchemy text()), and classify each one as parameterized, concatenated, or uncertain.&lt;/p&gt;

&lt;p&gt;The important thing inlet does not do: claim exploitability. A concatenated hit is a shape worth looking at, not a proven bug — pairing it with execution-based verification (like secfix) or manual review is still required. Saying that plainly, instead of implying more confidence than a static pass can honestly claim, is the whole point of the tool.&lt;/p&gt;

&lt;p&gt;The wall here is structural: a single-function AST pass can't resolve a query string built in one function and passed into another — true resolution needs interprocedural dataflow analysis, which is its own project-scale undertaking. So instead of guessing, that case returns uncertain, and there's a dedicated fixture (builder_function_uncertain.py) and test proving it does exactly that rather than silently getting it wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pattern, stated plainly&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Four tools, four walls, same discipline each time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Write the thing that tries to break your own claim before you write the feature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Let the tool fail loudly — uncertain, not_reproduced, opaque_escape_hatch — instead of rounding up to confidently wrong.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Put the limitation in the README above the fold, tied to the specific test that proves it, not as a footnote.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these four tools is trying to be the biggest thing in its category. Each one is trying to be exactly as trustworthy as its README claims to be — no more, no less.&lt;/p&gt;

&lt;p&gt;Repos: github.com/balbaks/secfix · github.com/balbaks/husk · github.com/balbaks/witness · github.com/balbaks/inlet&lt;/p&gt;

</description>
      <category>security</category>
      <category>testing</category>
      <category>tools</category>
    </item>
    <item>
      <title>husk: I extracted the sandbox from my last project, and made the tests do the talking</title>
      <dc:creator>Hassan Balbakie</dc:creator>
      <pubDate>Tue, 08 Sep 2026 20:39:20 +0000</pubDate>
      <link>https://dev.to/balbaks/husk-i-extracted-the-sandbox-from-my-last-project-and-made-the-tests-do-the-talking-41ml</link>
      <guid>https://dev.to/balbaks/husk-i-extracted-the-sandbox-from-my-last-project-and-made-the-tests-do-the-talking-41ml</guid>
      <description>&lt;p&gt;Where this came from&lt;/p&gt;

&lt;p&gt;A few weeks ago I shipped secfix — a tool that validates vulnerability scanner findings by actually executing the flagged code instead of guessing from the source. Semgrep finds something, secfix builds a harness with a tainted sentinel, runs it, and checks the execution trace to see if the "vulnerability" is real.&lt;/p&gt;

&lt;p&gt;That "run it somewhere it can't hurt me" piece was one Python module inside secfix: a locked-down Docker sandbox. It occurred to me that this piece was useful far beyond vuln validation — anyone running untrusted or AI-generated code needs exactly this, and most people either skip it or hand-roll something they never actually test.&lt;/p&gt;

&lt;p&gt;So I pulled it out, generalized it, and gave it one job: detonate untrusted Python safely, and prove it.&lt;/p&gt;

&lt;p&gt;What husk is&lt;br&gt;
python&lt;br&gt;
from husk import detonate&lt;/p&gt;

&lt;p&gt;result = detonate("print('hello from inside the sandbox')")&lt;br&gt;
print(result.stdout)      # hello from inside the sandbox&lt;br&gt;
print(result.exit_code)   # 0&lt;br&gt;
print(result.timed_out)   # False&lt;/p&gt;

&lt;p&gt;One function. Give it code (or a path to a script), get back stdout, stderr, exit code, whether it timed out, and how long it took. A thin CLI wraps the same thing:&lt;/p&gt;

&lt;p&gt;husk run script.py --timeout 10&lt;/p&gt;

&lt;p&gt;Under the hood, every run is --network none, non-root, --read-only rootfs with a small --tmpfs /tmp, --cap-drop=ALL, --security-opt=no-new-privileges, capped CPU/memory/PIDs, --rm, and a host-side timeout that kills the container rather than trusting it to terminate itself.&lt;/p&gt;

&lt;p&gt;None of that is novel. What I actually wanted to fix is more boring, and more important: most sandboxes never prove their own claims.&lt;/p&gt;

&lt;p&gt;The part that matters: adversarial tests, not a claims list&lt;/p&gt;

&lt;p&gt;It's easy to write a README that says "no network access" and never check whether a script running inside the container can actually get out. That gap is the whole ballgame — a security tool whose guarantees are untested is just marketing copy with extra steps.&lt;/p&gt;

&lt;p&gt;So before I wrote the runner, I wrote the attacks:&lt;/p&gt;

&lt;p&gt;attempt_network.py — tries three different outbound connections&lt;br&gt;
attempt_escape_fs.py — tries to write outside /tmp&lt;br&gt;
attempt_fork_bomb.py — bursts processes in a loop&lt;br&gt;
attempt_privesc.py — reaches for a capability-gated syscall&lt;br&gt;
run_forever.py — infinite loop, to prove the timeout actually fires&lt;br&gt;
benign.py — a normal script, the control case that has to succeed&lt;/p&gt;

&lt;p&gt;Then tests/test_isolation.py runs each one through detonate() and checks the specific failure mode, not just "something went wrong":&lt;/p&gt;

&lt;p&gt;The network test doesn't just check for a nonzero exit — it asserts the stderr shows a genuine connection-level failure (Network is unreachable, a gaierror, a URLError). A broken import accidentally "blocking" the network shouldn't be able to pass this test.&lt;br&gt;
The filesystem test asserts Read-only file system in stderr and that the write inside /tmp succeeded — read-only isolation shouldn't mean nothing works at all.&lt;br&gt;
The fork-bomb test parses the fixture's own summary line and asserts blocked &amp;gt; 0 and succeeded &amp;lt; attempted — throttled, not just crashed into something unrelated.&lt;br&gt;
The timeout test asserts the container was killed promptly by the host, not left to hit some unrelated resource ceiling.&lt;/p&gt;

&lt;p&gt;If any of these six tests fail, the tool doesn't ship. That's the actual gate — not "does it run hello world."&lt;/p&gt;

&lt;p&gt;A design choice I want to call out: no bind mount, ever&lt;/p&gt;

&lt;p&gt;Code goes into the container over stdin (docker run -i ... python3 -), not as a mounted file. There's no host path visible inside the container at all.&lt;/p&gt;

&lt;p&gt;This wasn't about convenience. The usual question with sandboxes — "which host paths are exposed, and can the code escape through them?" — doesn't get answered here, it gets removed. There's no mount to escape from and no mount config to quietly misconfigure six months from now.&lt;/p&gt;

&lt;p&gt;What husk explicitly does NOT protect against&lt;/p&gt;

&lt;p&gt;This part goes in the README above the fold, not buried at the bottom, because I think it's the actually useful information:&lt;/p&gt;

&lt;p&gt;It shares the host kernel. This is Docker — namespaces and cgroups, not a hypervisor boundary. It is not gVisor, Firecracker, or Kata, and makes no claim to be. If the code you're running might carry a working container-escape exploit, husk alone isn't enough.&lt;br&gt;
No side-channel defense. Nothing here mitigates cache-timing or Spectre-class attacks — a shared kernel and shared cores don't allow it.&lt;br&gt;
No dependency-install phase. v0.1.0 runs one self-contained script with the network fully off. A future "pip install these first" step needs network access and is its own supply-chain risk surface — one this tool doesn't cover today, and won't quietly start covering later without saying so loudly.&lt;br&gt;
It needs local Docker socket access, which is real friction inside a restricted or rootless CI environment.&lt;br&gt;
Try it / read the proof&lt;br&gt;
git clone &lt;a href="https://github.com/balbaks/husk" rel="noopener noreferrer"&gt;https://github.com/balbaks/husk&lt;/a&gt;&lt;br&gt;
cd husk&lt;br&gt;
pip install -e .&lt;br&gt;
husk run examples/benign.py&lt;/p&gt;

&lt;p&gt;The claims above are only worth what the tests are worth, so don't take my word for it — read tests/test_isolation.py directly.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/balbaks/husk" rel="noopener noreferrer"&gt;https://github.com/balbaks/husk&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is branch #2 in a small, ongoing series of honest security tools. Branch #1 was secfix, which refuses to call a vulnerability "fixed" unless a fresh execution trace proves it — and which documents in detail exactly where that approach hits a wall on real framework code (spoiler: inferring a Django view's full data dependencies is where solo-scope work ends and team-scale integration begins).&lt;/p&gt;

&lt;p&gt;Same principle both times: say what's verified. Name what isn't.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>python</category>
      <category>security</category>
      <category>software</category>
    </item>
    <item>
      <title>I built a vulnerability scanner that refuses to lie to me</title>
      <dc:creator>Hassan Balbakie</dc:creator>
      <pubDate>Mon, 24 Aug 2026 17:35:51 +0000</pubDate>
      <link>https://dev.to/balbaks/i-built-a-vulnerability-scanner-that-refuses-to-lie-to-me-22fl</link>
      <guid>https://dev.to/balbaks/i-built-a-vulnerability-scanner-that-refuses-to-lie-to-me-22fl</guid>
      <description>&lt;p&gt;Static analysis tools have a trust problem. They read your source, pattern-match something that looks injectable, and hand you a warning — plus, increasingly, an AI-generated "fix." You have no idea if the warning is real or if the fix works. You just get more things to check.&lt;/p&gt;

&lt;p&gt;I wanted the opposite: a tool that doesn't guess, doesn't trust the AI's word, and doesn't call anything fixed until it's proven fixed. So I built secfix.&lt;/p&gt;

&lt;p&gt;The idea: proof by execution&lt;/p&gt;

&lt;p&gt;Given a finding from Semgrep, secfix doesn't re-read the code. It runs it. It builds a test harness that calls the flagged function with a unique tainted marker, executes it inside a locked-down Docker sandbox, and records exactly where that marker ends up. If the marker lands unescaped inside a SQL string, or in a shell command, or in a file path that escapes its directory — that's a confirmed vulnerability, demonstrated by execution, not inferred from syntax.&lt;/p&gt;

&lt;p&gt;Then it patches the function, runs the same harness again on a fresh trace, and only calls the fix validated if the marker now lands somewhere safe. Crucially: it never trusts the model's own description of what it changed. The AI's account of the fix is irrelevant — only a fresh execution trace counts. If re-verification doesn't come back clean, the patch is reported as unvalidated, full stop.&lt;/p&gt;

&lt;p&gt;The whole thing is built to fail closed. When it can't prove something, it says uncertain — it never rounds up to "safe."&lt;/p&gt;

&lt;p&gt;Where it broke — and why that's the interesting part&lt;/p&gt;

&lt;p&gt;The tool passed all its tests. But passing your own fixtures proves very little. So I pointed it at real vulnerable Django apps.&lt;/p&gt;

&lt;p&gt;It hit a wall immediately. Framework code isn't like a plain function — you can't just import a Django view and call it. The moment you touch it, Django wants settings, an app registry, a database, a request object. My harness imported the module and crashed before it reached a single line of the actual vulnerability.&lt;/p&gt;

&lt;p&gt;I could have documented that as a limitation and moved on. Instead I went after it, one wall at a time: detecting the framework and its required Python version, matching the sandbox base image to it, calling django.setup(), baking a migrated database into the container, and constructing a real HTTP request with Django's own RequestFactory.&lt;/p&gt;

&lt;p&gt;And it worked — I got a real Django view SQL-injection all the way to a confirmed verdict, the tainted input landing unparameterized in executed SQL, proven by running it.&lt;/p&gt;

&lt;p&gt;The honest conclusion&lt;/p&gt;

&lt;p&gt;But reaching that verdict took something I'm not going to pretend away: I had to hand-feed the tool knowledge it couldn't derive on its own — the specific database row the view expected, and a framework-internal file-size threshold buried in Django's upload handling. Some walls generalized cheaply. Others didn't — and the ones that didn't are exactly the ones that separate a clever solo project from the multi-year integration problem that funded security companies work on with whole teams.&lt;/p&gt;

&lt;p&gt;I wrote all of it down — which walls fell easily, which didn't, and precisely where "automatic" stops. That map is the real deliverable. Not a demo where everything works, but an honest boundary of what execution-based vulnerability reproduction can and can't do on its own today.&lt;/p&gt;

&lt;p&gt;The code, the safety design, and the full investigation are here: &lt;a href="https://www.github.com/balbaks/secfix" rel="noopener noreferrer"&gt;https://www.github.com/balbaks/secfix&lt;/a&gt; — with the Django deep-dive on the v1-django-bootstrap-spike branch.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>docker</category>
      <category>security</category>
      <category>testing</category>
    </item>
    <item>
      <title>I built a provenance protocol, audited my own auth, found 2 CRITICALs</title>
      <dc:creator>Hassan Balbakie</dc:creator>
      <pubDate>Thu, 23 Jul 2026 10:45:50 +0000</pubDate>
      <link>https://dev.to/balbaks/i-built-a-provenance-protocol-audited-my-own-auth-found-2-criticals-1de5</link>
      <guid>https://dev.to/balbaks/i-built-a-provenance-protocol-audited-my-own-auth-found-2-criticals-1de5</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Provenance Protocol, Audited My Own Auth, and Found 2 CRITICALs
&lt;/h1&gt;

&lt;p&gt;I spent weeks building VERITAS — a protocol for verifiable provenance, identity, and attestation. 25 authenticated endpoints. Ed25519 signatures. Six layers: identity, content provenance, agent trust, economic escrow, governance.&lt;/p&gt;

&lt;p&gt;Then I audited my own auth. It was broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  CRITICAL #1: Every signed request was replayable forever
&lt;/h2&gt;

&lt;p&gt;I had Ed25519 signatures on all mutating endpoints. That proves the sender controls the private key. What it doesn't prove is that the request is &lt;em&gt;fresh&lt;/em&gt;. A captured &lt;code&gt;(did, message, signature)&lt;/code&gt; tuple was valid indefinitely. Someone with access to a single logged request could replay it a thousand times.&lt;/p&gt;

&lt;p&gt;The fix: a 60-second timestamp window. Every request now includes an ISO-8601 timestamp. The server rejects anything outside the window. A signature from two minutes ago is dead. Replay protection isn't a feature you add later — it's part of the signature verification itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  CRITICAL #2: The server trusted caller-supplied messages
&lt;/h2&gt;

&lt;p&gt;This one was worse. Every endpoint accepted a &lt;code&gt;message&lt;/code&gt; field from the client, then verified the signature against it. The problem: the &lt;code&gt;message&lt;/code&gt; the client signed and the parameters the server actually acted on were two different things. A client could sign &lt;code&gt;"amount=10"&lt;/code&gt; while submitting &lt;code&gt;amount=1000&lt;/code&gt; in the body. The signature would verify — because it verified the &lt;em&gt;message&lt;/em&gt;, not the &lt;em&gt;operation&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The fix: canonical payload binding. The server now reconstructs the message from the actual request parameters it's about to act on. Keys are sorted lexicographically. Values are serialized with a deterministic float format. The signature is verified against the reconstructed message. If the body says &lt;code&gt;amount=1000&lt;/code&gt;, the reconstructed message says &lt;code&gt;amount=1000&lt;/code&gt;, and the signature must match that — not whatever the caller claims it should be. The &lt;code&gt;message&lt;/code&gt; field was deleted from every request model entirely. That field was the vulnerability.&lt;/p&gt;

&lt;p&gt;The implementation is in &lt;code&gt;identity/did.py::canonical_message()&lt;/code&gt;. The critical line is the float serialization: &lt;code&gt;f"{float(v):.8f}".rstrip("0").rstrip(".")&lt;/code&gt; — if your client uses &lt;code&gt;str(v)&lt;/code&gt; or &lt;code&gt;repr(v)&lt;/code&gt; instead, you get mystifying 403s. That one line is documented in the README because it's the thing that will bite anyone implementing a client.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meta-test that prevents regression
&lt;/h2&gt;

&lt;p&gt;Fixing bugs is easy. Proving they stay fixed is harder. I wrote a route-enumerating meta-test that programmatically discovers every authenticated endpoint, sends a request with a bad signature to each one, and asserts 403. If I ever add a new endpoint and forget to wire auth, the test fails. The two CRITICALs above can't silently return because the test suite won't let them.&lt;/p&gt;

&lt;p&gt;18 tests total. 4 unit tests on the trust scoring engine. 14 integration tests covering auth rejection, replay protection, dispute resolution, and the full governance→arbiter→escrow→agent trust chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo that proves it works end-to-end
&lt;/h2&gt;

&lt;p&gt;A six-second asciinema recording showing all six layers firing together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An AI agent exceeds its 100-credit delegation cap&lt;/li&gt;
&lt;li&gt;The cap is enforced at the agent endpoint (403)&lt;/li&gt;
&lt;li&gt;The violation routes through escrow&lt;/li&gt;
&lt;li&gt;A dispute is filed&lt;/li&gt;
&lt;li&gt;Carol is elected arbiter through a real governance cycle&lt;/li&gt;
&lt;li&gt;Carol resolves the dispute&lt;/li&gt;
&lt;li&gt;The agent's trust score drops 50→40 because its side lost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every step is a real HTTP call. The demo is a shell script in the repo. Anyone can clone and run it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;I shipped with no test suite. Don't. The first time Claude audited my code, it found 11 issues in one pass — missing auth on 20+ endpoints, private keys stored in the database, proof validation that counted instead of verifying. Every single one was fixed, and every fix got a test. But I should have written the tests first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;github.com/balbaks/veritas — please break it.&lt;/p&gt;

&lt;p&gt;The threat model and known challenges are in docs/SPEC.md. The demo runs with &lt;code&gt;bash scripts/demo.sh&lt;/code&gt;. If you find a vulnerability, open an issue. Public credit for every verified finding.&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>opensource</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
