<?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: Oyinlola Lawal</title>
    <description>The latest articles on DEV Community by Oyinlola Lawal (@lawaloyinlola).</description>
    <link>https://dev.to/lawaloyinlola</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%2F1256620%2F70f19a67-b6df-493e-b555-4ea22934943d.jpeg</url>
      <title>DEV Community: Oyinlola Lawal</title>
      <link>https://dev.to/lawaloyinlola</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lawaloyinlola"/>
    <language>en</language>
    <item>
      <title>A pentest lab on Apple Silicon (and what a frontend dev did with root)</title>
      <dc:creator>Oyinlola Lawal</dc:creator>
      <pubDate>Sat, 03 Oct 2026 20:41:18 +0000</pubDate>
      <link>https://dev.to/lawaloyinlola/a-pentest-lab-on-apple-silicon-and-what-a-frontend-dev-did-with-root-27k</link>
      <guid>https://dev.to/lawaloyinlola/a-pentest-lab-on-apple-silicon-and-what-a-frontend-dev-did-with-root-27k</guid>
      <description>&lt;p&gt;In the first post of this series I said the lab and the language came next: how to build a sealed environment where you can break things freely. This is the lab. I built it, ran a full engagement through it against a deliberately vulnerable target, took the box to root two different ways, and wrote the whole thing up as a guide anyone on an Apple Silicon Mac can follow. Then, because old habits die hard, I defaced the server with a pure-CSS animation. More on that at the end.&lt;/p&gt;

&lt;p&gt;If you own an M1, M2, M3, M4, or in my case an M5, this is the write-up I wish I had found before I lost an afternoon to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem nobody warns you about
&lt;/h2&gt;

&lt;p&gt;Most pentest lab guides quietly assume you are on an Intel machine running VirtualBox. Follow one on an Apple Silicon Mac and it breaks at step one, for a reason that is rarely stated plainly.&lt;/p&gt;

&lt;p&gt;The vulnerable machines the whole training ecosystem is built around are x86. Metasploitable, most of VulnHub, the classic teaching targets, all of them. Your Mac is ARM. VirtualBox virtualises, it does not emulate a different processor architecture, so an x86 image simply will not boot on it. Parallels and VMware Fusion are more polished, but they are built to virtualise too: they run guests that share your CPU's architecture, which is great for ARM Linux or Windows on ARM and no help at all for an old x86 target. Parallels also costs money.&lt;/p&gt;

&lt;p&gt;So the real obstacle has nothing to do with hacking. It is that an ARM host and an x86 target speak different instruction sets, and the entire learning ecosystem lives on the far side of that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one insight the whole lab hangs on
&lt;/h2&gt;

&lt;p&gt;The fix is a single tool that does two different jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Virtualise&lt;/strong&gt; ARM-native guests, so they run at near-native speed. Perfect for &lt;strong&gt;Kali Linux&lt;/strong&gt;, the attacking machine, which ships an official ARM64 build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emulate&lt;/strong&gt; x86 guests, translating a foreign architecture one instruction at a time. Slower, but the only way to boot an old x86 target like &lt;strong&gt;Metasploitable 2&lt;/strong&gt; on an ARM Mac.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://mac.getutm.app/" rel="noopener noreferrer"&gt;UTM&lt;/a&gt;, a free and open-source front-end for QEMU, does both. That single capability is the entire reason this lab works, and every other setup decision follows from it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Guest&lt;/th&gt;
&lt;th&gt;UTM mode&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Attacker&lt;/td&gt;
&lt;td&gt;Kali Linux (ARM64)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Virtualize&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native speed, snappy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target&lt;/td&gt;
&lt;td&gt;Metasploitable 2 (x86)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Emulate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The only way to boot x86 on ARM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pick the wrong mode and the VM will not boot. That is the number-one beginner mistake, and once the virtualise-versus-emulate split is in your head the rest is mechanical. Kali is ARM, so you virtualise it. The target is x86, so you emulate it. Both sit on one isolated Shared Network, and the whole thing runs comfortably on a 16 GB laptop. The emulated target is slower than real hardware, but for running scans and shells rather than compiling kernels, it is perfectly usable.&lt;/p&gt;

&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%2Fe251e803g6xqhozchurt.png" 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%2Fe251e803g6xqhozchurt.png" alt="One Apple Silicon Mac running UTM, with a virtualised ARM Kali attacker and emulated x86 targets on a shared, isolated 192.168.64.0/24 network." width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No paid software, no Intel hardware, no cloud bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The phases of a pentest, walked end to end
&lt;/h2&gt;

&lt;p&gt;A lab is only worth building if you run a real process through it, and hacking is not random. In the first post I laid out the seven stages a real engagement follows. This lab was my chance to walk all seven against a live target instead of reading about them, so here is what each one actually looked like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Pre-engagement.&lt;/strong&gt; The target is mine. It runs on a self-owned, isolated network with nothing else in scope, and I took a clean disk snapshot before touching anything so I could reset to a known state. On a client engagement this stage is the written permission and the rules of engagement. In a lab it is still the first thing you do, because "I own this and nobody else can reach it" is the whole reason the rest is legal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Reconnaissance and 3. Scanning.&lt;/strong&gt; &lt;code&gt;nmap -sV&lt;/code&gt; mapped the open ports and the version behind each service. Two jumped out: an ancient file-transfer service on port 21, and an equally ancient SSH on 22. &lt;code&gt;searchsploit&lt;/code&gt; then checked those versions against Kali's offline copy of Exploit-DB, and an OpenVAS scan produced the kind of severity-ranked report a client actually expects. The discipline here is the lesson: you let the tools surface the attack surface first, then you decide what is worth pursuing, rather than reaching straight for an exploit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Gaining access and 5. Maintaining access.&lt;/strong&gt; This is where the lab earns its keep, because I did it two completely different ways. The patient way and the instant way, which I will come back to in the next section. Once I was root I established persistence with a hidden account, so a foothold would survive even if the original hole were patched. In an ethical context that persistence is proof of impact, not a place to hide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Covering tracks.&lt;/strong&gt; A real attacker clears the logs to stay invisible. The ethical version is to study exactly how that is done, then describe it, because the defensive lesson is the valuable half. I truncated auth logs, web logs and shell history, and the point of writing it up is the opposite of hiding: if a server's &lt;code&gt;auth.log&lt;/code&gt; can be wiped from the box itself, that is the argument for shipping logs off to a SIEM in real time, where the attacker cannot reach them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Reporting.&lt;/strong&gt; The exploit is not the product. The report is. I wrote the findings up twice over in effect: a plain executive summary a manager with no security background can act on, and the engineer-level detail underneath it, with a fix and a verification step for every finding. Stages two through six are indistinguishable from a breach. The first stage and the last, permission before and explanation after, are the entire difference between a penetration test and a crime with good documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways in, and why I did both
&lt;/h2&gt;

&lt;p&gt;I deliberately walked the target two different routes, because they teach opposite lessons.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;manual path&lt;/strong&gt; is the honest picture of how most real compromises start. I pointed Hydra at SSH with a small custom wordlist and it found four accounts where the password was just the username, which is one of the most common findings in real engagements. I logged in as an ordinary user, enumerated the box, and then climbed to administrator three separate ways: a sloppy &lt;code&gt;sudo&lt;/code&gt; rule, an old &lt;code&gt;nmap&lt;/code&gt; binary with an interactive mode that drops to a shell, and a world-writable &lt;code&gt;/etc/passwd&lt;/code&gt;. Slower, noisier, and every step maps cleanly to a control that would have stopped it.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;automated path&lt;/strong&gt; is the other extreme. Metasploit fired a single module at that old file-transfer service, which shipped with a backdoor baked into its source (vsftpd 2.3.4, CVE-2011-2523), and handed me a root session in seconds with no password at all. Fast, but brittle: it depends on one specific flaw being present and unpatched.&lt;/p&gt;

&lt;p&gt;Both end in the same place, full control, which is exactly the point. One is a patient chain of small mistakes, the other a single unpatched service, and knowing both is the difference between running a tool and understanding what it does. Once you have root, the post-exploitation steps are identical, so in the guide the automated path just points back to the manual one for the shared work rather than repeating it.&lt;/p&gt;

&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%2Fjizrgb628lqflos2ccu5.png" 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%2Fjizrgb628lqflos2ccu5.png" alt="Two independent paths to root: a slow manual credential chain and a one-shot automated backdoor, converging on the same full control." width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when you give a frontend developer an exploit
&lt;/h2&gt;

&lt;p&gt;Here is the part I did not expect to enjoy as much as I did.&lt;/p&gt;

&lt;p&gt;The loudest thing an attacker can do to a web server is deface it, replace the homepage with their own. It is the one action that guarantees incident response gets called, which is exactly why, in a real attack, it comes last: after persistence, after the credential theft, after the logs are wiped. By then the quiet work is done and the attacker is either leaving a calling card or does not care about being seen.&lt;/p&gt;

&lt;p&gt;In real attacks that calling card is usually crude. A black background, some green text, a logo, done. But I did not come to security from nowhere. I came from building interfaces, and when I had root on a web server and a blank page to fill, I could not bring myself to paste in ugly HTML.&lt;/p&gt;

&lt;p&gt;So the defacement became a small, slightly ridiculous demonstration of exactly the skill I was pivoting away from. I backed up the original page first, because preserving it is both professional practice and my rollback path, and then I replaced it with a page that types out "I AM LAWAL" letter by letter in a bold Orbitron display face with a neon glow, mirrors each character of my name backwards one at a time, closes the gap between "I" and "AM" so it resolves into "IAM", and settles into a compromise banner over CRT scanlines. No JavaScript. The entire sequence is &lt;code&gt;@keyframes&lt;/code&gt; driven by per-character CSS custom properties, every beat tunable from a single block at the top, and it collapses gracefully to the finished state for anyone with reduced-motion turned on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fznxjnon23moojmag7lxp.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fznxjnon23moojmag7lxp.gif" alt="The pure-CSS defacement animation: " width="479" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The pure-CSS defacement, no JavaScript: "I AM LAWAL" types out, each letter mirrors backwards, and the gap closes into "IAM" over a compromise banner.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It is, objectively, far too much effort for a proof-of-concept nobody asked to look nice. That is also the point. The defacement reads as a joke about my own background, but writing it was the same instinct that runs underneath the whole lab: wanting to know how the machine actually works. Re-enabling precisely the legacy SSH algorithms a 2008-era server needs without the one that newer OpenSSH has removed, choosing UTM because of a virtualise-versus-emulate distinction most guides skip, and hand-building a pure-CSS animation to announce a compromise are all the same habit. Understand the mechanism, and you find both where it breaks and how to fix it.&lt;/p&gt;

&lt;p&gt;Then I restored the original page, because an ethical hacker undoes what he changed and hides nothing he found.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four things that cost me the most time
&lt;/h2&gt;

&lt;p&gt;A good lab guide is not the happy path, it is the gotchas. These four are the ones worth saving you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UTM has no snapshot button.&lt;/strong&gt; Coming from VirtualBox I went hunting for a Snapshots menu that does not exist in the GUI. The answer lives one layer down in QEMU's own disk tooling (&lt;code&gt;qemu-img snapshot&lt;/code&gt;), and a crucial detail: UTM's "run without saving" discards only the VM's memory, not changes written to disk. If you deface the web server, "run without saving" will not bring it back. You restore the snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modern SSH refuses to talk to a 2008-era server.&lt;/strong&gt; Current OpenSSH disables the old key-exchange and host-key algorithms the target offers, so every SSH tool fails until you re-enable them in a host-scoped &lt;code&gt;~/.ssh/config&lt;/code&gt; block. Leave &lt;code&gt;ssh-dss&lt;/code&gt; out, because newer OpenSSH has removed DSA entirely and including it throws a different error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The privilege-escalation trick needs a space.&lt;/strong&gt; The old SUID &lt;code&gt;nmap&lt;/code&gt; route is &lt;code&gt;! sh&lt;/code&gt; with a space on this build. The no-space form silently does nothing. Small detail, twenty-minute time sink.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old Ubuntu uses the &lt;code&gt;admin&lt;/code&gt; group, not &lt;code&gt;sudo&lt;/code&gt;.&lt;/strong&gt; On Ubuntu 8.04 the sudo-granting group is &lt;code&gt;admin&lt;/code&gt;, so &lt;code&gt;-G sudo&lt;/code&gt; when creating a persistence account fails silently because that group does not exist yet. Always check: &lt;code&gt;grep -E '^(sudo|admin)' /etc/group&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anyone can run &lt;code&gt;nmap&lt;/code&gt;. The value in a guide is the twenty-minute problems that the guides skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every attack step has a defensive mirror
&lt;/h2&gt;

&lt;p&gt;I wrote the whole engagement so that each offensive action ends with the control that would have stopped it, because that is the half that matters once you are back at work.&lt;/p&gt;

&lt;p&gt;The seven findings are not seven unrelated problems. Default passwords, a backdoored service, trivial escalation, plaintext secrets, reversible password hashing, erasable logs, they mostly exist because the software was frozen in time on an operating system years past end of life. Fix the age and the neglect and whole classes of finding vanish at once. The defensive mirror writes itself from there: remove default accounts and require strong unique passwords with lockout and MFA, patch or remove the vulnerable service and keep a patch schedule, enforce least-privilege &lt;code&gt;sudo&lt;/code&gt; and drop needless SUID bits, move secrets into a manager, use a modern slow hash with a locked-down shadow file, and ship logs off-box to a SIEM with tamper alerting.&lt;/p&gt;

&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%2Fiiq4k72kmur0b79dkvbn.png" 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%2Fiiq4k72kmur0b79dkvbn.png" alt="Seven findings by severity and fix effort, all resolving to one root cause: an unsupported, unpatched operating system." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to try it
&lt;/h2&gt;

&lt;p&gt;The full step-by-step lab, the environment build, scanning, both attack paths, every gotcha, and the matching defensive lessons, is written up as a &lt;a href="https://lawaloyinlola.com/security/labs/building-a-pentest-lab-on-apple-silicon" rel="noopener noreferrer"&gt;complete guide&lt;/a&gt; you can follow command by command, and the &lt;a href="https://lawaloyinlola.com/files/apple-silicon-pentest-lab-report.pdf" rel="noopener noreferrer"&gt;findings report I wrote from it&lt;/a&gt; shows the other half: the same engagement turned into something a manager and an engineer can both act on. The challenge target, DevGuru, is next.&lt;/p&gt;

&lt;p&gt;If you are on an Apple Silicon Mac and you have been putting this off because the guides do not fit your hardware, they do not have to. Reach for UTM, keep the virtualise-versus-emulate split in your head, and you are past the part that stops most people. The rest is just process, and the process is the profession.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>security</category>
      <category>pentesting</category>
      <category>applesilicon</category>
    </item>
    <item>
      <title>A security checklist my coding agent has to run</title>
      <dc:creator>Oyinlola Lawal</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:52:42 +0000</pubDate>
      <link>https://dev.to/lawaloyinlola/a-security-checklist-my-coding-agent-has-to-run-7b</link>
      <guid>https://dev.to/lawaloyinlola/a-security-checklist-my-coding-agent-has-to-run-7b</guid>
      <description>&lt;p&gt;Most security checklists are written in a way that guarantees they will be ignored, and the first one I wrote was one of those. It listed things that should be true about an application. Sessions should be invalidated when a password changes. Uploads should be validated by their content rather than their filename. Every line was correct, and I never tested a single one, because a sentence that says what should be true never says what you would do to find out.&lt;/p&gt;

&lt;p&gt;That is the whole failure mode. You read the item, you agree with it, and agreeing feels close enough to checking that you tick it and move on. Nothing in the format ever forces the moment where you find out you were wrong.&lt;/p&gt;

&lt;p&gt;So I rewrote the thing from the other end. Every control now comes with a step you run, and a control is not done until that step has been run and given back the result it promised.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the rewrite looks like
&lt;/h2&gt;

&lt;p&gt;Here is the declarative version of a session control, the kind you find in most checklists:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Verify that session tokens are invalidated when the password is changed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And here is what it became, lightly trimmed from control 9:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Kill every session on password change or reset, not just the current one. The whole point of a reset is evicting an attacker who already has a live session, so leaving their other sessions valid defeats it. Store a &lt;code&gt;sessionsValidAfter&lt;/code&gt; timestamp per user and reject anything issued earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt; log in on two browsers, change the password in one, refresh the other. Expect a 401.&lt;/p&gt;
&lt;/blockquote&gt;

&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%2Fyuzfn6gig0glszzujm1w.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%2Fyuzfn6gig0glszzujm1w.jpg" alt="The two-browser test: log in on two browsers, change the password in one, and the other should get a 401 on its next request. If it stays logged in, that is the bug." width="799" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second version takes thirty seconds to act on, and it can fail. That is the only difference that matters, and it is the difference between a checklist and a test.&lt;/p&gt;

&lt;p&gt;Doing the same to every control gave me the constraint the whole project hangs on: one verification step per control, with no exceptions. There are 82 controls across three skills and 82 verification steps, and you do not have to take my word for either number, because it is countable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;skills/&lt;span class="k"&gt;*&lt;/span&gt;/SKILL.md&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="si"&gt;))&lt;/span&gt;&lt;span class="s2"&gt;: controls=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-cE&lt;/span&gt; &lt;span class="s1"&gt;'^### [0-9]+\.'&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; verify=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-cE&lt;/span&gt; &lt;span class="s1"&gt;'^[[:space:]]*- \*\*Verify:\*\*'&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="c"&gt;# legal-compliance: controls=20 verify=20&lt;/span&gt;
&lt;span class="c"&gt;# project-kickoff: controls=18 verify=18&lt;/span&gt;
&lt;span class="c"&gt;# security-protocols: controls=44 verify=44&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The totals on their own are not enough, and this is how I found out. The first version of this loop only compared them, and they matched while control 12 had no verify step at all and control 15 had two, so both sides still read 44. So the totals are now the summary, not the proof: a per-control check, &lt;a href="https://github.com/lawalOyinlola/appsec-protocols/blob/main/ci/scripts/check-verify-steps.sh" rel="noopener noreferrer"&gt;&lt;code&gt;ci/scripts/check-verify-steps.sh&lt;/code&gt;&lt;/a&gt;, fails any control without exactly one step, and it runs on every pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it is a skill and not a document
&lt;/h2&gt;

&lt;p&gt;A document gets read when you remember it exists, which is usually after the code is written. By then a finding means a rewrite, and a rewrite has to argue with a deadline. The same finding before the first line is written is a decision, and a decision costs nothing.&lt;/p&gt;

&lt;p&gt;That timing is why the checklist ships as an agent skill: an instruction file that a coding agent such as Claude Code loads on its own when it is about to write the kind of code the file covers. When mine is about to write authentication, database access, an upload handler, an API endpoint, a payment flow, an LLM call or deploy configuration, it loads the &lt;a href="https://github.com/lawalOyinlola/appsec-protocols/blob/main/skills/security-protocols/SKILL.md" rel="noopener noreferrer"&gt;security skill&lt;/a&gt; first and writes the code against it. Before a launch, it runs the whole thing as an audit and writes a result for each control to a file, which is a different and more honest thing to hand someone than a green summary.&lt;/p&gt;

&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%2F16eg3qtucrokfrvv5xer.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%2F16eg3qtucrokfrvv5xer.jpg" alt="An excerpt from the audit file: control 20, security headers, marked FAIL; control 21, force HTTPS, marked PASS, with a note listing what was not checked." width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part of the audit the skill wrote when I ran it against this site. The missing headers have since been added.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It is also built so it cannot tell me what I want to hear. Its own reporting rule forbids "all secure" as an output. It reports which controls pass, which fail, and which were never checked, because unchecked is a real state, and hiding it inside a pass is how a checklist turns back into theatre.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it covers
&lt;/h2&gt;

&lt;p&gt;The controls are grouped by the phase of the work where they have to be enforced, not by severity, so the group you need is the one you are already working in.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Secrets and keys.&lt;/strong&gt; Where a secret lives, who can read it today, what your commit history still remembers after you deleted it, and why the client bundle is never the answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data access.&lt;/strong&gt; Row level security with a real policy rather than just switched on, an ownership check on every record lookup, encryption at rest for the columns that deserve it, and whitelisted fields so a request body cannot set &lt;code&gt;role&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity and sessions.&lt;/strong&gt; Cookie flags, password hashing with a real work factor, killing every session on password change, JWT verification that pins the algorithm, and OAuth configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting.&lt;/strong&gt; Login and password reset, then the rest of the API, which is the part almost everyone skips, plus bot protection checked on the server rather than trusted from a widget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input and output.&lt;/strong&gt; Parameterised queries, the NoSQL operator injection that parameterising does not fix, schema validation at the boundary, output encoding where the data is used, uploads checked by magic bytes, and API responses serialised on purpose rather than handed a database row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transport and supply chain.&lt;/strong&gt; Security headers, HTTPS and HSTS, a CORS allowlist that is not a reflected origin, dependency scanning that fails the build, and vetting the package itself, since a scanner knows nothing about a malicious package that is three days old.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request surface.&lt;/strong&gt; CSRF where cookies authenticate, request size caps at every layer, the endpoints you never metered, the ones you never meant to publish, and the staging environment running production data behind a guessable URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI features.&lt;/strong&gt; Prompt injection, treated as something to survive rather than something to prevent with careful wording, model output handled as untrusted input, and usage caps, because a metered endpoint with no ceiling is a bill somebody else gets to write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your own agent.&lt;/strong&gt; The skills, plugins and MCP servers you install run with your permissions, and a skill that fetches its instructions from a URL is remote code you have not reviewed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Injection beyond SQL.&lt;/strong&gt; Path traversal, SSRF including the cloud metadata endpoint, shell commands built from user input, and deserialisation formats that execute code as a side effect of parsing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operations.&lt;/strong&gt; Audit trails with an actor and an IP, secrets kept out of logs and logs kept out of public reach, alerts that reach a person, backups you have actually restored, least privilege, tenant isolation, webhook signature checks, and payment decisions that stay on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The gate itself.&lt;/strong&gt; Branch protection that includes administrators, required checks that match the jobs that actually run, and actions pinned by commit hash, because every control enforced in CI assumes nobody can route around CI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two sibling skills sit next to it in the same repository, in the same format and under the same rule about verification: one for legal and compliance questions, and one for the decisions that have to exist before the first feature commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;It is a baseline, not a threat model. Working through all 44 security controls does not make an application secure. It makes 44 specific and common failures less likely, which is worth having and is not the same claim.&lt;/p&gt;

&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%2F8oqmbkkw64l4pjlsgrle.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%2F8oqmbkkw64l4pjlsgrle.jpg" alt="The 44 controls cover a small, specific area. Threat modelling, business logic, cryptographic design, physical and personnel security and industry regulation all sit outside it." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It does not cover threat modelling for your particular product, business logic flaws, cryptographic design, physical or personnel security, or anything specific to your industry's regulations. The legal skill is not legal advice and does not pretend to be, and where the exposure is real it tells you to go and find a lawyer. The CI configuration in the repository checks the subset of controls that a machine can check and nothing more, so a green pipeline is evidence about that subset and silence about the rest.&lt;/p&gt;

&lt;p&gt;The full version of all of that is in &lt;a href="https://github.com/lawalOyinlola/appsec-protocols/blob/main/DISCLAIMER.md" rel="noopener noreferrer"&gt;DISCLAIMER.md&lt;/a&gt;, and it is worth reading before you use any of this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Take it
&lt;/h2&gt;

&lt;p&gt;Everything is public and MIT licensed at &lt;a href="https://github.com/lawalOyinlola/appsec-protocols" rel="noopener noreferrer"&gt;github.com/lawalOyinlola/appsec-protocols&lt;/a&gt;. Take the whole thing, take one group, or take the two lines that catch the bug you actually have. The controls are mapped to OWASP ASVS 5.0 in the repository, if you want to see where the coverage sits and where it does not. If you run it against something real and a control does not survive contact, tell me, because that is the feedback it needs.&lt;/p&gt;

&lt;p&gt;If you only take one thing, take the two-browser test. Log in twice, change your password in one window, and refresh the other. If it is still logged in, your password reset does not evict anyone, including the person it was meant for.&lt;/p&gt;

</description>
      <category>security</category>
      <category>appsec</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>From zero to tech lead: what the AltSchool year actually taught me</title>
      <dc:creator>Oyinlola Lawal</dc:creator>
      <pubDate>Mon, 21 Sep 2026 08:48:24 +0000</pubDate>
      <link>https://dev.to/lawaloyinlola/from-zero-to-tech-lead-what-the-altschool-year-actually-taught-me-i8k</link>
      <guid>https://dev.to/lawaloyinlola/from-zero-to-tech-lead-what-the-altschool-year-actually-taught-me-i8k</guid>
      <description>&lt;p&gt;It has been over a year since I graduated from &lt;strong&gt;AltSchool Africa&lt;/strong&gt;, and what a transformative ride it has been. From having no background in tech to leading software projects at &lt;strong&gt;Tech N' Goodwill Limited&lt;/strong&gt;, my journey has been packed with lessons, mistakes, growth, and soft skills development.&lt;/p&gt;

&lt;h2&gt;
  
  
  It started with a casual chat
&lt;/h2&gt;

&lt;p&gt;It all started with a simple, casual conversation. I was wrapping up my national service year, uncertain about what the future held. During a chat with a friend about starting a career in tech, he mentioned &lt;a href="https://altschoolafrica.com" rel="noopener noreferrer"&gt;AltSchool&lt;/a&gt; and even hinted at scholarships. I did not have a clue about tracks or specializations, but I knew I loved solving problems and creating websites that looked good. Frustrated by poorly designed apps and websites, I gravitated toward &lt;strong&gt;frontend engineering&lt;/strong&gt;. PS: I did not get a scholarship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The months before the program started
&lt;/h2&gt;

&lt;p&gt;On &lt;strong&gt;27 October 2022&lt;/strong&gt; I registered, took the entrance exam, got in, and joined the Slack community. That is where the real magic began. I was anxiously waiting for the program to kick off, and days turned into weeks, weeks into months. While waiting, I dove into the community, sharing ideas, asking questions, and soaking up everything I could. Someone recommended the W3Schools Discord community, which turned out to be a treasure trove, filled with brilliant minds, eager to help and share.&lt;/p&gt;

&lt;p&gt;That community taught me a vital lesson: &lt;strong&gt;do not wait for the perfect moment, start now&lt;/strong&gt;. I took some courses on Udemy, especially Jonas Schmedtmann's &lt;a href="https://www.udemy.com/course/design-and-develop-a-killer-website-with-html5-and-css3/" rel="noopener noreferrer"&gt;HTML and CSS course&lt;/a&gt;, which was my first real step into coding.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the sessions began
&lt;/h2&gt;

&lt;p&gt;Once the sessions started in March 2023, I was attending live classes, consuming online resources, and studying with external materials. Our tutors were incredible, constantly urging us to read ahead, explore documentation, and learn independently. They kept telling us, "Things will not always be this slow; the real work is coming." They were right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two hours before the deadline, my code was gone
&lt;/h2&gt;

&lt;p&gt;Fast forward a few months, and the first semester exams and assessment projects arrived. Like many, I procrastinated, relaxing and learning but not actively building. When I finally started, I finished the project and was about to submit. I made my first commit, pushed to GitHub, then wanted to make some tweaks. I successfully pushed my code, but it was not showing up on GitHub. I kept checking, sweating, frustrated, until I realised my codebase on my computer was gone. The repo was empty. Less than two hours before the deadline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;E shock me!&lt;/strong&gt; What now? Give up? Call it a day? Nope. I grabbed my laptop and started from scratch. That moment turned out to be my biggest lesson. I rebuilt about 70% of the project in just an hour, spotting errors, correcting them, simplifying my code. I was more proud of that second version than the first. I submitted just before the deadline, exhausted but victorious. Later I emailed the facilitators asking for an extension. Luckily I was not the only one who needed extra time, and I got it, which let me finish the project properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the community taught me that the courses did not
&lt;/h2&gt;

&lt;p&gt;What I loved most about this journey was the AltSchool community: meeting brilliant minds, peers, tutors, mentors, facilitators, instructors, circle members, guest speakers, and organizers. The soft skills I learned, writing, content creation, collaboration and leadership, were just as vital as coding.&lt;/p&gt;

&lt;p&gt;I remember thinking HTML and CSS were enough to conquer the world. In Setemi's unmistakable voice: "boom!". His guidance, recommendations, and relentless pushes kept me going through the toughest moments. He would tell us to read ahead, explore documentation and learn independently, and he meant it. His advice was not just about coding, it was about developing the mindset to grow beyond limits. One of the most impactful resources he shared was the &lt;a href="https://www.coursera.org/learn/learning-how-to-learn" rel="noopener noreferrer"&gt;Learning How to Learn&lt;/a&gt; course on Coursera by Deep Teaching Solutions, an eye-opener that transformed how I approached learning.&lt;/p&gt;

&lt;h2&gt;
  
  
  JavaScript, Vue and React
&lt;/h2&gt;

&lt;p&gt;In the following semesters I dove into JavaScript, Vue and React. I also took more of Jonas Schmedtmann's courses, &lt;a href="https://www.udemy.com/course/the-complete-javascript-course/" rel="noopener noreferrer"&gt;The Complete JavaScript Course&lt;/a&gt; and &lt;a href="https://www.udemy.com/course/the-ultimate-react-course/" rel="noopener noreferrer"&gt;The Ultimate React Course&lt;/a&gt;. These took me from a complete beginner to a confident developer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The capstone: ScissorsWeb
&lt;/h2&gt;

&lt;p&gt;My capstone project was a URL shortening web app called ScissorsWeb. It shortens URLs with custom text or emoji aliases, generates QR codes, and handles authentication, database storage using &lt;strong&gt;Supabase&lt;/strong&gt;, analytics, error management, theme modes, and animations with GSAP, all built with React and TypeScript. It was a fun, challenging way to put everything I had learned into practice. You can read &lt;a href="https://lawaloyinlola.com/projects/scissorsweb" rel="noopener noreferrer"&gt;the case study&lt;/a&gt; or try &lt;a href="https://scissorsweb.netlify.app/" rel="noopener noreferrer"&gt;the live app&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Graduating
&lt;/h2&gt;

&lt;p&gt;Fifty two weeks of learning, over 120 live sessions, and a pile of assessments later, the diploma arrived.&lt;/p&gt;

&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%2F5cgxmve3jcjkkjw66uuv.png" 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%2F5cgxmve3jcjkkjw66uuv.png" alt="AltSchool Africa diploma in Frontend Engineering awarded to Lawal Oyinlola Ibrahim, dated 29 February 2024" width="800" height="618"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The graduation email put a number on it: 95 points out of 100 across all categories of progress tracking, weighted heavily toward the capstone project.&lt;/p&gt;

&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%2Fnyn851hjlc6twh6ax1ta.png" 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%2Fnyn851hjlc6twh6ax1ta.png" alt="Graduation email from AltSchool Africa confirming a total of 95 points out of 100 across all categories of progress tracking" width="800" height="1039"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I am now
&lt;/h2&gt;

&lt;p&gt;Since then I have been building production software rather than coursework. I am a Software Developer and Team Lead at Tech N' Goodwill Limited, where much of my work went into SafulPay, a fintech product in Sierra Leone, across the mobile app, the agent and merchant dashboards, and the corporate site.&lt;/p&gt;

&lt;p&gt;Right now I am building &lt;a href="https://trakkam.com" rel="noopener noreferrer"&gt;Trakkam&lt;/a&gt;, a GPS and IoT platform for fleet monitoring: live tracking, alerts, and a web dashboard, plus a WhatsApp bot for the people who would rather ask a question in a chat than open another app. It is self hosted, so the infrastructure, the deployments and the hardening are mine to get right too.&lt;/p&gt;

&lt;p&gt;That is also what changed what I am curious about. Building payment software and then hosting a tracking platform yourself makes you think hard about what happens when someone attacks it, so I have started working through security properly and writing down what I find. I measured &lt;a href="https://lawaloyinlola.com/blog/what-my-waf-actually-blocked" rel="noopener noreferrer"&gt;what a web application firewall actually blocked&lt;/a&gt; in front of an app I had already built, and wrote up &lt;a href="https://lawaloyinlola.com/blog/ethical-hacking-is-not-a-toolset" rel="noopener noreferrer"&gt;the vocabulary and the rules of ethical hacking&lt;/a&gt; for anyone starting where I am. More of that is coming, including the lab work behind it.&lt;/p&gt;

&lt;p&gt;The through line from the empty repo two hours before a deadline is the same one I would offer anyone starting out: the moment something breaks is usually the moment you actually learn the thing. Keep building, and keep writing down what breaks.&lt;/p&gt;

</description>
      <category>career</category>
      <category>beginners</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>What my WAF actually blocked, and what my app already stops on its own</title>
      <dc:creator>Oyinlola Lawal</dc:creator>
      <pubDate>Mon, 14 Sep 2026 15:21:37 +0000</pubDate>
      <link>https://dev.to/lawaloyinlola/what-my-waf-actually-blocked-and-what-my-app-already-stops-on-its-own-2ij2</link>
      <guid>https://dev.to/lawaloyinlola/what-my-waf-actually-blocked-and-what-my-app-already-stops-on-its-own-2ij2</guid>
      <description>&lt;p&gt;A request arrives at your server for &lt;code&gt;/api/vehicles/1' OR '1'='1&lt;/code&gt;. Before a single line of your application code runs, something in front of it reads that string, recognises the shape of a SQL injection attempt, and answers 403.&lt;/p&gt;

&lt;p&gt;That something is a web application firewall, and last month I spent a weekend trying to work out whether mine was actually earning its place.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, what the thing actually is
&lt;/h2&gt;

&lt;p&gt;It is software. Twenty years ago you might have racked a physical appliance for this, but today a WAF is one of three things, all of them software: a module inside the web server you already run, a standalone reverse proxy that traffic passes through on its way in, or a cloud service like Cloudflare that your DNS points at, filtering requests at its own edge before anything reaches your machines at all.&lt;/p&gt;

&lt;p&gt;Mine is the first kind. ModSecurity, running as a module inside the NGINX that already sits at the edge of the application, which means no extra network hop, no new machine, and no new bill. The rules it runs come from the OWASP Core Rule Set, a community-maintained collection of patterns for injection, cross-site scripting, traversal and the rest. ModSecurity is the engine; the rule set is the fuel.&lt;/p&gt;

&lt;p&gt;The distinction that matters is which layer it reads. A network firewall works at the level of IP addresses and ports, so it can decide that traffic from this address to port 443 is allowed, and that is the end of its knowledge. It cannot see inside the request. A WAF works one layer up, reading the URL, the headers, the JSON body, the cookies. That is the entire reason it can spot the payload in the first paragraph, where a network firewall would see a perfectly ordinary POST to a perfectly ordinary port and wave it through.&lt;/p&gt;

&lt;h2&gt;
  
  
  And why you would want one
&lt;/h2&gt;

&lt;p&gt;Three honest reasons, in the order they actually matter.&lt;/p&gt;

&lt;p&gt;The first is defence in depth. If a real vulnerability exists in your code, a WAF can block the exploit request before it reaches the vulnerable line, which buys you time between a flaw existing and that flaw being fixed. The second is virtual patching: when something you depend on has a disclosed vulnerability and you cannot ship a fix today, a rule blocking the known exploit pattern closes the door while you work. The third, and the reason a great many WAFs exist at all, is that an auditor asked for one. PCI DSS effectively requires it for systems handling card data.&lt;/p&gt;

&lt;p&gt;Notice what is absent from that list. Nothing there says a WAF makes your application secure. It is a compensating control, which means it compensates for a weakness rather than removing one, and holding onto that distinction is the whole point of what follows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;I put it in front of the same production Next.js and NestJS application I already gate with a CI/CD security pipeline. That pipeline catches problems in code before it ships; this was the runtime half of the same argument, and running both against one real system beats running either against a tutorial target.&lt;/p&gt;

&lt;p&gt;The setup was a local rig, because attacking a client's production system is precisely what you do not do. Two listeners in front of a production-mode build of the app, one with the rule engine live, one with it switched off. Same TLS, same proxy path, same everything, except that single switch.&lt;/p&gt;

&lt;p&gt;That second listener is the reason any of this counts as a measurement rather than a demonstration. Without it, a blocked request is only a claim. With it, a block counts only when the identical payload gets through on the other port, which makes every number in the rest of this piece a difference between two observations rather than an assertion.&lt;/p&gt;

&lt;p&gt;One detail that took longer than it should have: the app had to run in production mode. In development it relaxes its own defences, turning CSRF off and dropping the secure flag on cookies, so measuring a WAF against a development build would have compared it to a weaker application than the real one and flattered the result. Production behaviour, synthetic data. That combination was most of the setup work.&lt;/p&gt;

&lt;p&gt;Then twenty six attack payloads across six classes, run three times: once with no WAF, once with it watching and logging but not blocking, once with it live. Then tuned, then re-run at a higher paranoia setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number that looked like a win
&lt;/h2&gt;

&lt;p&gt;At the tuned setting the firewall blocked twelve of the twenty six.&lt;/p&gt;

&lt;p&gt;My first instinct, and very nearly the sentence I published, was that all twelve were payloads the application had already handled by itself. It is a clean line. It is also not quite what the data said, and finding that out was the most useful hour of the project.&lt;/p&gt;

&lt;p&gt;So I went back to the baseline phase, the one with no WAF at all, and looked up what the application had done with those same twelve. Five of them it had already rejected outright: a UUID validator killing a malformed identifier before any query ran, an explicit date check on a history endpoint returning 400 before the value reached the database. Real rejections. The firewall blocking those afterwards added nothing at all.&lt;/p&gt;

&lt;p&gt;The other seven told a different story, because the application's baseline answer to those was not a rejection. It was 200, or 201. It had accepted the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why accepting a payload is not the same as being vulnerable
&lt;/h2&gt;

&lt;p&gt;This is where it stopped being disappointing and started being interesting. The cross-site scripting payloads, both reflected and stored, were accepted by the application every time in the baseline. That sounds alarming until you follow what happens next. The reflected ones get rendered back to the browser and React escapes them on the way out, so what appears on screen is inert text rather than running code. The stored ones get written to the database exactly as submitted, and then sit there, because nothing in the application ever executes a database field as code.&lt;/p&gt;

&lt;p&gt;Both of those are genuinely safe outcomes, and neither of them is a rejection. The safety happens downstream, after acceptance.&lt;/p&gt;

&lt;p&gt;Which is exactly where the seven earned their place. The firewall stopped those payloads in transit, before they were ever reflected or written at all. The application would have been fine without it, because its own safety net was waiting further down. But the WAF closed the gap one step earlier, and that matters more than it sounds, because downstream safety nets are precisely the thing that quietly breaks when somebody refactors a template eighteen months from now and does not know why the escaping was there. The firewall does not care about that refactor. It never let the payload in.&lt;/p&gt;

&lt;p&gt;So the honest version is narrower and more useful than my first draft: five of twelve were pure overlap, seven were a real second layer in front of controls that already held. Both true at once, and only one of them survived into the first summary I wrote.&lt;/p&gt;

&lt;h2&gt;
  
  
  The class that never appeared in either column
&lt;/h2&gt;

&lt;p&gt;Then there is the part that needed no correcting, because the numbers were identical from the start.&lt;/p&gt;

&lt;p&gt;I seeded two synthetic tenants and tried to read one tenant's data while logged in as the other. Same route, same valid session, a different vehicle identifier in the URL. With the firewall on: 404. With it off: 404. Every variant, every time.&lt;/p&gt;

&lt;p&gt;That is not the WAF failing quietly. It is the WAF having no job to do. A request for someone else's record is, byte for byte, indistinguishable from a request for your own. There is no pattern in it that says attack, because nothing about its shape is malicious. The only thing separating the two is a fact living in a database row rather than in the bytes on the wire, and a signature-matching filter was never built to know that fact. The application's own authorisation check, one condition in a query confirming the record belongs to the requester's organisation, was the entire defence in both columns.&lt;/p&gt;

&lt;p&gt;That class is called broken object level authorisation, or insecure direct object reference in the older naming. It is mundane to exploit, since it requires changing one value in a URL, and it appears in breach writeups constantly for exactly the reason above: it is invisible to this kind of control by construction.&lt;/p&gt;

&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%2Flawaloyinlola.com%2Fimages%2Fblog%2Fwhat-my-waf-actually-blocked%2Fwhat-stops-each-class.svg" 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%2Flawaloyinlola.com%2Fimages%2Fblog%2Fwhat-my-waf-actually-blocked%2Fwhat-stops-each-class.svg" alt="What actually stops each attack class: application, WAF, or neither" width="960" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The tuning, which was not where I expected
&lt;/h2&gt;

&lt;p&gt;WAF folklore says the false positives will come from content. Long tokens, JSON bodies full of coordinates, names with apostrophes tripping an injection rule.&lt;/p&gt;

&lt;p&gt;None of that happened at paranoia level one. What happened instead was structural. The Core Rule Set blocks the PATCH and DELETE methods by default, and this is a REST API that depends on both, so with blocking switched on and nothing tuned, every legitimate profile edit and record update returned 403. A user renaming their own vehicle got a firewall error.&lt;/p&gt;

&lt;p&gt;The fix was one targeted exclusion restoring those verbs for the API paths, and the discipline there is worth naming: fix the specific rule, never reach for the paranoia dial. Turning the sensitivity down would have fixed the same false positive while silently giving back real detection elsewhere, and you would never know which. After the exclusion, the legitimate PATCH returned 200 and all twelve attack blocks were still in place. That is what a surgical fix looks like when you can prove it.&lt;/p&gt;

&lt;p&gt;Raising the paranoia level to two then answered a question I would otherwise have guessed at. It caught nothing the tuned level one had missed. It fired more rules on attacks already being blocked, and it introduced two fresh false positives on legitimate traffic: a strong password that happened to contain a comment sequence, and a business name containing an ampersand. More paranoia, in this specific application, was pure cost.&lt;/p&gt;

&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%2Flawaloyinlola.com%2Fimages%2Fblog%2Fwhat-my-waf-actually-blocked%2Fparanoia-tradeoff.svg" 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%2Flawaloyinlola.com%2Fimages%2Fblog%2Fwhat-my-waf-actually-blocked%2Fparanoia-tradeoff.svg" alt="The tuning curve: attacks blocked stays flat, false positives do not" width="960" height="560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One genuine bypass did turn up. Injection payloads the firewall caught in a query string or JSON body sailed past it when the identical payload sat in a URL path segment instead, because the core injection rules inspect arguments rather than path segments. In this application the UUID validator rejects those anyway, so nothing was exposed. Against an application that consumed a raw path segment, that is a live bypass at every level I tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not cover
&lt;/h2&gt;

&lt;p&gt;Rate abuse and user enumeration were scoped out. Both are decided by the application's own throttler and the wording of its responses rather than by any rule at this paranoia level, so there was nothing for a WAF-versus-control comparison to isolate. I also did not test a cloud WAF, so nothing here transfers automatically to Cloudflare or AWS WAF, whose rule sets and defaults differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  Something to run
&lt;/h2&gt;

&lt;p&gt;If you want the thirty second version of the finding that mattered most, go looking for the pattern no firewall will catch for you. In an ORM-backed codebase, that means finding every query that fetches a record by identifier without also constraining it to the caller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"findFirst&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;findUnique&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;findByPk"&lt;/span&gt; src/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-vi&lt;/span&gt; &lt;span class="s2"&gt;"organizationid&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;tenantid&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;userid&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;ownerid"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line that comes back is a query trusting an identifier from the request without checking who is asking. Most will be fine and deliberately scoped elsewhere. The ones that are not are the class of bug that identical 404s in both my columns proved a firewall will never see.&lt;/p&gt;




&lt;p&gt;Twelve blocked requests looked like twelve wins. Five were the application agreeing with itself through an extra layer. Seven were the firewall genuinely doing something the code could not do until later. And the class that decides most real breaches never touched either column, because it was never a pattern-matching problem to begin with.&lt;/p&gt;

&lt;p&gt;A WAF buys you time. It does not buy you a query that checks who is asking.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>devops</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Ethical hacking is not a toolset, it is a mindset with a permission slip</title>
      <dc:creator>Oyinlola Lawal</dc:creator>
      <pubDate>Thu, 10 Sep 2026 15:56:41 +0000</pubDate>
      <link>https://dev.to/lawaloyinlola/ethical-hacking-is-not-a-toolset-it-is-a-mindset-with-a-permission-slip-436a</link>
      <guid>https://dev.to/lawaloyinlola/ethical-hacking-is-not-a-toolset-it-is-a-mindset-with-a-permission-slip-436a</guid>
      <description>&lt;p&gt;Most confusion around ethical hacking is vocabulary, not difficulty. People use hacker, ethical hacker and penetration tester interchangeably, then argue past each other about what is legal and what is not. The distinctions are simple once they are laid out, and they matter, because one of them is the difference between an invoice and a criminal record.&lt;/p&gt;

&lt;p&gt;This is the first of a short series. It covers three things: the mindset, the method and the styles. The lab and the language, meaning how to build a sealed environment to practise in and how to live on the Linux command line, come next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mindset
&lt;/h2&gt;

&lt;p&gt;Start with the uncomfortable part. An ethical hacker and a criminal have the same skill set. The same tools, the same techniques, often the same curiosity about how a thing comes apart. The difference is not capability. It is motivation and permission.&lt;/p&gt;

&lt;p&gt;Take the locksmith and the burglar. Both can open your door. One was invited, tests the lock, and tells you which one is weak. The other takes your television. Skill is neutral. Intent and authorisation are not.&lt;/p&gt;

&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%2Ffk7d41t44zq8irvbr76p.png" 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%2Ffk7d41t44zq8irvbr76p.png" alt="Same skill set, one difference: permission" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The three hats
&lt;/h3&gt;

&lt;p&gt;The naming comes from old westerns, where the hero wore white and the villain wore black.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;White hat.&lt;/strong&gt; The professional. Works with permission, inside an agreed scope, reports what he finds and helps fix it. This is the locksmith, and this is the target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grey hat.&lt;/strong&gt; Pokes at systems nobody asked him to touch. Often means well, sometimes even reports the flaw afterwards, and still breaks the law doing it. My honest read on grey hats is less generous than the textbook one: a grey hat to me is simply a black hat caught in the act and in denial. Good intentions discovered after the fact are not the same as permission obtained before it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Black hat.&lt;/strong&gt; The criminal. Not a distant abstraction either. He is over your shoulder while you type, in the bin behind your office, or already sitting on your network, quietly.&lt;/p&gt;

&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%2Fwd1cibopz89fvyy1aq2s.png" 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%2Fwd1cibopz89fvyy1aq2s.png" alt="The three hats and where each one stands on permission" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One correction worth making while we are here. Hackers do not wear hoodies, do not work exclusively at night, and are not defined by a stock photo. The people doing this professionally look like the people doing any other engineering job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ethical hacking is not the same as penetration testing
&lt;/h3&gt;

&lt;p&gt;These two get used interchangeably and they are not the same size.&lt;/p&gt;

&lt;p&gt;Ethical hacking is the whole thing: the mindset, the skill set and the process of looking for weakness with permission. It is ongoing and open ended.&lt;/p&gt;

&lt;p&gt;Penetration testing is ethical hacking with a scope of work. It has an agreed target, a start date, an end date, and a report at the end of it.&lt;/p&gt;

&lt;p&gt;Every penetration test is ethical hacking. Not every piece of ethical hacking is a penetration test.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one rule that keeps you employable
&lt;/h2&gt;

&lt;p&gt;Never touch a system you do not have written permission to test.&lt;/p&gt;

&lt;p&gt;No permission, no test. That is the whole rule, and it is worth more than any tool you will learn.&lt;/p&gt;

&lt;p&gt;Skill does not make an action legal. Permission does. The exact same scan, run with the same command, against the same kind of target, is a paid engagement on a client's network and a crime on a stranger's. Nothing about the technique changes. Only the paperwork does.&lt;/p&gt;

&lt;p&gt;This applies further than people expect. Not a friend's website because he said it was fine over WhatsApp. Not a company you admire and want to impress. Not a login page you stumbled onto and got curious about. Get it in writing, every time, and keep the writing.&lt;/p&gt;

&lt;p&gt;Two ideas carry that rule in practice. The rules of engagement set out what you may test, when you may test it, how far you may go, and who to call when something breaks. The scope is the fence: the exact list of targets you are allowed to touch. Anything outside the fence is off limits even when it looks easy, and especially when it looks easy. Find something new mid engagement and you ask first and wait for a yes. When you are not sure, you stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The method
&lt;/h2&gt;

&lt;p&gt;Hacking is not random. Real engagements follow a path, and each stage feeds the next. What you learn while looking around decides what you scan, and what you scan decides what you try to break.&lt;/p&gt;

&lt;p&gt;Seven stages, start to finish:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-engagement.&lt;/strong&gt; Agree the rules, the scope and the permission, in writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconnaissance.&lt;/strong&gt; Gather everything you can about the target, mostly without touching it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scanning.&lt;/strong&gt; Probe for open doors: live hosts, ports, services, versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access.&lt;/strong&gt; Use what you found to get in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintaining access.&lt;/strong&gt; Hold that foothold long enough to prove impact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Covering tracks.&lt;/strong&gt; Understand how an attacker would hide the evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reporting.&lt;/strong&gt; Write down everything, clearly, for the people who have to fix it.&lt;/li&gt;
&lt;/ol&gt;

&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%2Fi1zhxe9yz1jgxfqucgg8.png" 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%2Fi1zhxe9yz1jgxfqucgg8.png" alt="The seven stages, with the two an attacker never has" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look closely at that list and one thing stands out. Stages two through six are exactly what an attacker does. Same sequence, same tools, often the same afternoon. Judge by the middle of the list alone and a penetration test and a breach are indistinguishable.&lt;/p&gt;

&lt;p&gt;What actually separates the two is the first stage and the last. The pentester asks permission before, and explains everything after. The attacker does neither. Remove pre-engagement and reporting and you are not doing security work, you are committing an offence with good documentation habits.&lt;/p&gt;

&lt;p&gt;Stage six deserves a footnote, because it reads strangely in an ethical context. A real attacker covers his tracks to stay hidden. An ethical hacker studies the technique so he can describe it, then does the opposite: logs every step, records every change, and hands it all over. Anything you altered gets restored. Nothing you found gets hidden.&lt;/p&gt;

&lt;h2&gt;
  
  
  The styles
&lt;/h2&gt;

&lt;p&gt;Before a test starts, both sides agree how much of the map the tester gets. That single decision changes the cost, the timeline and the realism of the whole engagement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Black box.&lt;/strong&gt; Little to no information, sometimes just a company name or a domain. Closest to what a genuine outsider faces, and the slowest, because a good chunk of the budget goes on discovering things the client already knew.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grey box.&lt;/strong&gt; Some information, often a normal user account and a rough idea of the architecture. The practical middle ground, and the one most engagements land on, because it simulates the realistic threat of an attacker who already has a foothold or a stolen credential.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;White box.&lt;/strong&gt; Everything: network diagrams, credentials, configuration, sometimes the source code. The fastest and most thorough option, and the best value when the goal is coverage rather than theatre.&lt;/p&gt;

&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%2Ffvy7vpxu4iyb0jpi6y2l.png" 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%2Ffvy7vpxu4iyb0jpi6y2l.png" alt="Black box, grey box and white box, by how much you are told" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is a temptation to treat black box as the serious option because it feels most like a real attack. It is not automatically the better buy. If the goal is to find as many real weaknesses as possible in a fixed number of days, telling the tester more usually finds more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can actually be tested
&lt;/h2&gt;

&lt;p&gt;A penetration test is not one activity. The target decides the tools, the techniques and the skills you need.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;What it covers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;Servers, firewalls, internal and external infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web application&lt;/td&gt;
&lt;td&gt;Websites, portals, APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile&lt;/td&gt;
&lt;td&gt;Android and iOS applications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wireless&lt;/td&gt;
&lt;td&gt;Wi-Fi networks and their authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;AWS, Azure, GCP configuration and identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Social engineering&lt;/td&gt;
&lt;td&gt;Phishing and the human layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configuration review&lt;/td&gt;
&lt;td&gt;Settings, builds, hardening baselines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Everything else&lt;/td&gt;
&lt;td&gt;IoT, hardware, physical access&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Coming from frontend and full stack engineering, the web application and cloud rows are where existing knowledge transfers most directly, and that is deliberately where I am aiming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things worth holding onto
&lt;/h2&gt;

&lt;p&gt;Permission is not paperwork you clear on the way to the interesting part. It is the thing that makes the interesting part legal.&lt;/p&gt;

&lt;p&gt;The process is the profession. Anyone can run a scanner. Scoping the work properly and writing a report someone can act on is the part that gets paid for.&lt;/p&gt;

&lt;p&gt;The report is the product. Nobody is buying the exploit. They are buying the explanation of how it happened and what to change.&lt;/p&gt;

&lt;p&gt;Next in this series: the lab and the language. How to build a sealed environment where you can break things freely, and the handful of Linux commands you end up living inside.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>security</category>
      <category>appsec</category>
      <category>penetrationtesting</category>
    </item>
  </channel>
</rss>
