<?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: CodeAnt AI</title>
    <description>The latest articles on DEV Community by CodeAnt AI (codeant).</description>
    <link>https://dev.to/codeant</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%2Forganization%2Fprofile_image%2F14435%2F778cf53b-5258-4ba0-abc5-d0dfad9d950d.png</url>
      <title>DEV Community: CodeAnt AI</title>
      <link>https://dev.to/codeant</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codeant"/>
    <language>en</language>
    <item>
      <title>Why Your $6,500 Penetration Test Will Cost You More Than the $47,000 One</title>
      <dc:creator>Amartya Jha</dc:creator>
      <pubDate>Wed, 09 Sep 2026 11:09:58 +0000</pubDate>
      <link>https://dev.to/codeant/why-your-6500-penetration-test-will-cost-you-more-than-the-47000-one-2fhe</link>
      <guid>https://dev.to/codeant/why-your-6500-penetration-test-will-cost-you-more-than-the-47000-one-2fhe</guid>
      <description>&lt;p&gt;Two penetration testing quotes land in your inbox.&lt;/p&gt;

&lt;p&gt;One says &lt;strong&gt;$6,500&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The other says &lt;strong&gt;$22,000&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Both vendors say they use AI. Both promise comprehensive coverage. Both have credible-looking reports, experienced consultants, and a convincing sales call.&lt;/p&gt;

&lt;p&gt;So why is one more than 3× the price?&lt;/p&gt;

&lt;p&gt;Because &lt;strong&gt;“penetration test” isn't a standardized unit of work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The difference usually isn't the PDF at the end. It's everything that happens before the PDF exists: how much of the attack surface gets mapped, how deeply authentication and authorization are tested, whether findings are manually validated, whether vulnerabilities are chained into attack paths, whether source code is reviewed, and what happens after the report lands.&lt;/p&gt;

&lt;p&gt;This matters even more when the pentest is being used for compliance, enterprise security reviews, or a customer audit.&lt;/p&gt;

&lt;p&gt;A cheap assessment can absolutely be useful. A $6,500 test isn't automatically bad, just as a $22,000 test isn't automatically good.&lt;/p&gt;

&lt;p&gt;The question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What are you actually buying at each price point?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Quick Numbers
&lt;/h2&gt;

&lt;p&gt;For planning purposes, penetration testing in 2026 can range from roughly &lt;strong&gt;$3,000 for a limited external assessment to $100,000+ for a large, integrated engagement&lt;/strong&gt; covering applications, APIs, infrastructure, source code, and multiple testing perspectives.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Penetration test type&lt;/th&gt;
&lt;th&gt;Typical 2026 planning range&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Limited black box / external assessment&lt;/td&gt;
&lt;td&gt;$3,000–$8,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full black box web application assessment&lt;/td&gt;
&lt;td&gt;$8,000–$25,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gray box / authenticated assessment&lt;/td&gt;
&lt;td&gt;$10,000–$40,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;White box / source code assessment&lt;/td&gt;
&lt;td&gt;$15,000–$60,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integrated black + gray + white box&lt;/td&gt;
&lt;td&gt;$25,000–$100,000+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continuous / recurring assessment&lt;/td&gt;
&lt;td&gt;$5,000–$20,000/month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These aren't standardized market prices. Actual pricing varies substantially by scope, application complexity, number of assets, number of roles, source-code size, infrastructure, methodology, tester involvement, and deliverables.&lt;/p&gt;

&lt;p&gt;And that's exactly why comparing two quotes by the final number alone doesn't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scope Is Usually Where the Difference Starts
&lt;/h2&gt;

&lt;p&gt;“Web application penetration test” sounds specific.&lt;/p&gt;

&lt;p&gt;It isn't.&lt;/p&gt;

&lt;p&gt;One vendor might mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One production application and its public login page.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another might mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The web application, API, authenticated endpoints, staging environment, cloud assets, JavaScript bundles, authentication flows, and all discovered subdomains.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are completely different engagements.&lt;/p&gt;

&lt;p&gt;Before comparing prices, turn the scope into something measurable.&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many applications are included?&lt;/li&gt;
&lt;li&gt;Which production and staging environments?&lt;/li&gt;
&lt;li&gt;Which domains and subdomains?&lt;/li&gt;
&lt;li&gt;How many APIs and endpoints?&lt;/li&gt;
&lt;li&gt;Are authenticated endpoints included?&lt;/li&gt;
&lt;li&gt;How many user roles?&lt;/li&gt;
&lt;li&gt;Is multi-tenant isolation tested?&lt;/li&gt;
&lt;li&gt;Is cloud infrastructure included?&lt;/li&gt;
&lt;li&gt;Are source repositories included?&lt;/li&gt;
&lt;li&gt;Are third-party integrations in scope?&lt;/li&gt;
&lt;li&gt;What is explicitly excluded?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A written scope document is more useful than a paragraph in a sales proposal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Methodology Depth Changes the Price
&lt;/h2&gt;

&lt;p&gt;The second major variable is &lt;strong&gt;how the testing is performed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two vendors can test the same application and produce very different results.&lt;/p&gt;

&lt;p&gt;A shallow engagement might look like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run automated scanners.&lt;/li&gt;
&lt;li&gt;Identify vulnerable versions.&lt;/li&gt;
&lt;li&gt;Match responses against known signatures.&lt;/li&gt;
&lt;li&gt;Generate findings.&lt;/li&gt;
&lt;li&gt;Deliver a PDF.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A deeper assessment might involve:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Passive reconnaissance.&lt;/li&gt;
&lt;li&gt;Attack-surface enumeration.&lt;/li&gt;
&lt;li&gt;Subdomain and cloud asset discovery.&lt;/li&gt;
&lt;li&gt;API endpoint mapping.&lt;/li&gt;
&lt;li&gt;Authentication testing.&lt;/li&gt;
&lt;li&gt;Authorization testing.&lt;/li&gt;
&lt;li&gt;Manual business-logic testing.&lt;/li&gt;
&lt;li&gt;Controlled exploitation.&lt;/li&gt;
&lt;li&gt;Privilege escalation.&lt;/li&gt;
&lt;li&gt;Cross-tenant access testing.&lt;/li&gt;
&lt;li&gt;Attack-path construction.&lt;/li&gt;
&lt;li&gt;Source-level investigation.&lt;/li&gt;
&lt;li&gt;Remediation validation.&lt;/li&gt;
&lt;li&gt;Retesting.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The target can be identical.&lt;/p&gt;

&lt;p&gt;The amount of work is not.&lt;/p&gt;

&lt;p&gt;That's why &lt;strong&gt;price alone is a poor proxy for pentest quality&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  $3,000–$8,000: Baseline External Testing
&lt;/h2&gt;

&lt;p&gt;At the lower end of the market, you're generally getting a limited external assessment.&lt;/p&gt;

&lt;p&gt;The exact methodology varies, but this tier may emphasize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automated vulnerability discovery&lt;/li&gt;
&lt;li&gt;Known-CVE detection&lt;/li&gt;
&lt;li&gt;Technology fingerprinting&lt;/li&gt;
&lt;li&gt;Basic port and service enumeration&lt;/li&gt;
&lt;li&gt;TLS configuration checks&lt;/li&gt;
&lt;li&gt;Common web vulnerabilities&lt;/li&gt;
&lt;li&gt;Structured reporting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This can be valuable for establishing baseline visibility.&lt;/p&gt;

&lt;p&gt;But buyers should check whether the engagement includes &lt;strong&gt;manual exploitation and validation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A scanner saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Potential SQL injection detected”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;is not equivalent to a tester demonstrating that a controllable parameter reaches a SQL query and validating the impact safely.&lt;/p&gt;

&lt;p&gt;The distinction is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;potential vulnerability vs. confirmed vulnerability.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the quote is inexpensive because manual validation isn't included, that's important to know before comparing it with a much more hands-on assessment.&lt;/p&gt;

&lt;h2&gt;
  
  
  $8,000–$25,000: Full External / Black Box Testing
&lt;/h2&gt;

&lt;p&gt;This is where the engagement can become a genuine methodology-driven external pentest.&lt;/p&gt;

&lt;p&gt;Depending on scope, testing may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DNS enumeration&lt;/li&gt;
&lt;li&gt;Subdomain discovery&lt;/li&gt;
&lt;li&gt;Certificate transparency analysis&lt;/li&gt;
&lt;li&gt;Port and service enumeration&lt;/li&gt;
&lt;li&gt;Web application fingerprinting&lt;/li&gt;
&lt;li&gt;JavaScript analysis&lt;/li&gt;
&lt;li&gt;Endpoint discovery&lt;/li&gt;
&lt;li&gt;API enumeration&lt;/li&gt;
&lt;li&gt;Authentication testing&lt;/li&gt;
&lt;li&gt;Authorization testing&lt;/li&gt;
&lt;li&gt;Common injection classes&lt;/li&gt;
&lt;li&gt;SSRF&lt;/li&gt;
&lt;li&gt;File upload vulnerabilities&lt;/li&gt;
&lt;li&gt;Access-control weaknesses&lt;/li&gt;
&lt;li&gt;Business-logic testing&lt;/li&gt;
&lt;li&gt;Cloud exposure&lt;/li&gt;
&lt;li&gt;Manual exploitation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important question isn't whether a proposal contains the word &lt;strong&gt;“manual.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ask &lt;strong&gt;what is actually manual&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Are authenticated API endpoints manually tested?&lt;/p&gt;

&lt;p&gt;Are authorization boundaries tested across roles?&lt;/p&gt;

&lt;p&gt;Are findings reproduced by a human tester?&lt;/p&gt;

&lt;p&gt;Are exploit chains investigated?&lt;/p&gt;

&lt;p&gt;Are business-logic workflows tested?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those questions reveal far more than a generic “OWASP methodology” statement.&lt;/p&gt;

&lt;h2&gt;
  
  
  $10,000–$40,000: Gray Box / Authenticated Testing
&lt;/h2&gt;

&lt;p&gt;Gray box testing introduces something external testing can't reproduce:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;legitimate application context.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of asking only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What can an anonymous attacker access?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;the tester can ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What can this authenticated user access that they shouldn't?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is where some of the most interesting authorization vulnerabilities appear.&lt;/p&gt;

&lt;p&gt;Consider a SaaS application with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admin&lt;/li&gt;
&lt;li&gt;Manager&lt;/li&gt;
&lt;li&gt;Employee&lt;/li&gt;
&lt;li&gt;Read-only user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and thousands of tenant-specific resources.&lt;/p&gt;

&lt;p&gt;Testing isn't simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/invoices/1234
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tester needs to establish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User A → Tenant A → Resource A
User A → Tenant B → Resource B
Manager → Admin functionality
Employee → Manager functionality
Read-only → Write functionality
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is to determine whether the application's &lt;strong&gt;authorization model matches the intended trust boundaries&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is where vulnerabilities such as BOLA/IDOR, privilege escalation, broken function-level authorization, and cross-tenant access become particularly important.&lt;/p&gt;

&lt;p&gt;And the number of roles and workflows can dramatically increase the testing effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  $15,000–$60,000: White Box / Source Code Testing
&lt;/h2&gt;

&lt;p&gt;Source access changes the problem entirely.&lt;/p&gt;

&lt;p&gt;A black box tester sees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP request
      ↓
HTTP response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A white box tester can investigate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP request
      ↓
Controller
      ↓
Middleware
      ↓
Authorization check
      ↓
Service
      ↓
Database query
      ↓
Dangerous sink
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes vulnerabilities visible that may be difficult or impossible to identify externally.&lt;/p&gt;

&lt;p&gt;A serious source-code assessment can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication implementation&lt;/li&gt;
&lt;li&gt;Authorization middleware&lt;/li&gt;
&lt;li&gt;API controllers&lt;/li&gt;
&lt;li&gt;Input validation&lt;/li&gt;
&lt;li&gt;Dataflow analysis&lt;/li&gt;
&lt;li&gt;Sensitive sinks&lt;/li&gt;
&lt;li&gt;Cryptographic usage&lt;/li&gt;
&lt;li&gt;Secrets&lt;/li&gt;
&lt;li&gt;Dependency usage&lt;/li&gt;
&lt;li&gt;Git history&lt;/li&gt;
&lt;li&gt;Infrastructure-as-code&lt;/li&gt;
&lt;li&gt;CI/CD configuration&lt;/li&gt;
&lt;li&gt;Security-sensitive framework configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The depth of this work matters enormously.&lt;/p&gt;

&lt;p&gt;“Source code included” doesn't necessarily mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Every line of the repository will be manually reviewed.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask what is actually being analyzed and how findings are traced back to the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  $25,000–$100,000+: Integrated Testing
&lt;/h2&gt;

&lt;p&gt;The most useful full assessments aren't simply:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;black box + gray box + white box.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They are integrated.&lt;/p&gt;

&lt;p&gt;Information discovered during one phase informs another.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;External recon
      ↓
API discovered
      ↓
Authenticated access obtained
      ↓
Authorization weakness identified
      ↓
Source code traced
      ↓
Attack path constructed
      ↓
Fix implemented
      ↓
Production retest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That produces a much stronger understanding of the actual risk than three disconnected reports.&lt;/p&gt;

&lt;p&gt;For a mid-sized SaaS application, an integrated engagement might fall somewhere around $25,000–$50,000.&lt;/p&gt;

&lt;p&gt;Larger applications with multiple services, environments, APIs, cloud infrastructure, and complex authorization models can move toward $50,000–$100,000+.&lt;/p&gt;

&lt;p&gt;Enterprise-scale testing can go beyond that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuous Testing: $5,000–$20,000/Month
&lt;/h2&gt;

&lt;p&gt;Annual pentesting has an obvious limitation.&lt;/p&gt;

&lt;p&gt;It tells you what was exposed &lt;strong&gt;when the test happened&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your engineering team deploys every week, the application tested in January may not resemble the application running in December.&lt;/p&gt;

&lt;p&gt;A new API gets deployed.&lt;/p&gt;

&lt;p&gt;A new subdomain appears.&lt;/p&gt;

&lt;p&gt;A cloud bucket gets created.&lt;/p&gt;

&lt;p&gt;An authentication flow changes.&lt;/p&gt;

&lt;p&gt;A service gets exposed.&lt;/p&gt;

&lt;p&gt;A dependency changes.&lt;/p&gt;

&lt;p&gt;The attack surface moves continuously.&lt;/p&gt;

&lt;p&gt;Recurring testing addresses that problem by shortening the time between changes and security validation.&lt;/p&gt;

&lt;p&gt;It doesn't necessarily replace a comprehensive periodic assessment.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Periodic pentest = deep assessment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continuous testing = ongoing validation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The two can complement each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost vs. Coverage
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engagement&lt;/th&gt;
&lt;th&gt;Typical range&lt;/th&gt;
&lt;th&gt;Strongest at finding&lt;/th&gt;
&lt;th&gt;Common gap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Automated assessment&lt;/td&gt;
&lt;td&gt;$3K–$8K&lt;/td&gt;
&lt;td&gt;Known technical weaknesses&lt;/td&gt;
&lt;td&gt;Business logic and attack chains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Black box&lt;/td&gt;
&lt;td&gt;$8K–$25K&lt;/td&gt;
&lt;td&gt;External attack surface&lt;/td&gt;
&lt;td&gt;Source-level vulnerabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gray box&lt;/td&gt;
&lt;td&gt;$10K–$40K&lt;/td&gt;
&lt;td&gt;Authorization and authenticated workflows&lt;/td&gt;
&lt;td&gt;Internal implementation details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;White box&lt;/td&gt;
&lt;td&gt;$15K–$60K&lt;/td&gt;
&lt;td&gt;Code-level vulnerabilities&lt;/td&gt;
&lt;td&gt;Runtime behavior without execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integrated&lt;/td&gt;
&lt;td&gt;$25K–$100K+&lt;/td&gt;
&lt;td&gt;Cross-layer attack paths&lt;/td&gt;
&lt;td&gt;Primarily constrained by scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continuous&lt;/td&gt;
&lt;td&gt;$5K–$20K/mo&lt;/td&gt;
&lt;td&gt;Changes introduced over time&lt;/td&gt;
&lt;td&gt;Depth depends on cadence and methodology&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important word in this table is &lt;strong&gt;gap&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every testing model sees some things better than others.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Finding Quality Test
&lt;/h2&gt;

&lt;p&gt;Here's one of the easiest ways to compare vendors.&lt;/p&gt;

&lt;p&gt;Ask for a &lt;strong&gt;redacted sample technical finding&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Look at what you're actually getting.&lt;/p&gt;

&lt;p&gt;A useful finding should tell an engineer:&lt;/p&gt;

&lt;h3&gt;
  
  
  What is vulnerable?
&lt;/h3&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Application may be vulnerable to authorization issues.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;GET /api/v1/invoices/{id}&lt;/code&gt; does not verify that the authenticated user belongs to the invoice's tenant.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How was it validated?
&lt;/h3&gt;

&lt;p&gt;Show the relevant request, response, prerequisites, and safe proof of impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does it happen?
&lt;/h3&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request
  ↓
Controller
  ↓
Invoice ID accepted
  ↓
Database lookup by ID
  ↓
Tenant ownership never checked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What is the impact?
&lt;/h3&gt;

&lt;p&gt;Can an attacker read another tenant's invoice?&lt;/p&gt;

&lt;p&gt;Modify it?&lt;/p&gt;

&lt;p&gt;Delete it?&lt;/p&gt;

&lt;p&gt;Access sensitive customer data?&lt;/p&gt;

&lt;h3&gt;
  
  
  How should it be fixed?
&lt;/h3&gt;

&lt;p&gt;Ideally, the remediation guidance points engineers toward the actual control that needs to change.&lt;/p&gt;

&lt;p&gt;This is where two reports with the same number of findings can have radically different value.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Attack-Path Question
&lt;/h2&gt;

&lt;p&gt;A pentest shouldn't always treat every vulnerability as an isolated row in a spreadsheet.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Low-privilege account
        ↓
Information disclosure
        ↓
Internal endpoint discovered
        ↓
Authorization bypass
        ↓
Sensitive API access
        ↓
Credential exposure
        ↓
Cloud privilege escalation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each individual finding might have a moderate severity.&lt;/p&gt;

&lt;p&gt;Together, they can represent a serious attack path.&lt;/p&gt;

&lt;p&gt;Ask the provider:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you identify and validate chained attack paths, or do you report vulnerabilities independently?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That answer can be more revealing than the number of tools listed in the proposal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Retest Question
&lt;/h2&gt;

&lt;p&gt;This is another major difference between quotes.&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is retesting included?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And more importantly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does “retest” actually mean?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A useful retest should verify that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The vulnerability is no longer exploitable.&lt;/li&gt;
&lt;li&gt;The underlying security control works as intended.&lt;/li&gt;
&lt;li&gt;The fix didn't introduce another bypass.&lt;/li&gt;
&lt;li&gt;The change is present in the environment that actually matters.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A staging retest doesn't necessarily prove that production is fixed.&lt;/p&gt;

&lt;p&gt;Also clarify how many retest cycles are included and whether they are billed separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compliance Deliverables Gap
&lt;/h2&gt;

&lt;p&gt;The pentest itself isn't always the only deliverable you need.&lt;/p&gt;

&lt;p&gt;Depending on the framework and auditor, you may need supporting evidence around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scope&lt;/li&gt;
&lt;li&gt;Testing dates&lt;/li&gt;
&lt;li&gt;Findings&lt;/li&gt;
&lt;li&gt;Severity&lt;/li&gt;
&lt;li&gt;Remediation&lt;/li&gt;
&lt;li&gt;Retesting&lt;/li&gt;
&lt;li&gt;Final status&lt;/li&gt;
&lt;li&gt;Risk acceptance&lt;/li&gt;
&lt;li&gt;Evidence handling&lt;/li&gt;
&lt;li&gt;Data retention&lt;/li&gt;
&lt;li&gt;Data deletion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't assume these documents are automatically included because the proposal says &lt;strong&gt;“compliance-ready report.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ask what you actually receive.&lt;/p&gt;

&lt;p&gt;And ask how sensitive information collected during testing is handled after the engagement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Checklist to Put Beside Every Quote
&lt;/h2&gt;

&lt;p&gt;Before signing a pentest SOW, make every vendor answer the same questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which applications?&lt;/li&gt;
&lt;li&gt;Which environments?&lt;/li&gt;
&lt;li&gt;Which domains and subdomains?&lt;/li&gt;
&lt;li&gt;Which APIs?&lt;/li&gt;
&lt;li&gt;Which third-party integrations?&lt;/li&gt;
&lt;li&gt;What's explicitly excluded?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are login flows tested?&lt;/li&gt;
&lt;li&gt;MFA?&lt;/li&gt;
&lt;li&gt;Password reset?&lt;/li&gt;
&lt;li&gt;Session management?&lt;/li&gt;
&lt;li&gt;Tokens?&lt;/li&gt;
&lt;li&gt;Account recovery?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Authorization&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Horizontal privilege escalation?&lt;/li&gt;
&lt;li&gt;Vertical privilege escalation?&lt;/li&gt;
&lt;li&gt;BOLA/IDOR?&lt;/li&gt;
&lt;li&gt;Cross-tenant access?&lt;/li&gt;
&lt;li&gt;Role boundaries?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Application logic&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manual business-logic testing?&lt;/li&gt;
&lt;li&gt;Workflow manipulation?&lt;/li&gt;
&lt;li&gt;Race conditions?&lt;/li&gt;
&lt;li&gt;Abuse cases?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS?&lt;/li&gt;
&lt;li&gt;Azure?&lt;/li&gt;
&lt;li&gt;GCP?&lt;/li&gt;
&lt;li&gt;Kubernetes?&lt;/li&gt;
&lt;li&gt;Containers?&lt;/li&gt;
&lt;li&gt;IAM?&lt;/li&gt;
&lt;li&gt;Cloud storage?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is source code included?&lt;/li&gt;
&lt;li&gt;How much is reviewed?&lt;/li&gt;
&lt;li&gt;Is dataflow analysis performed?&lt;/li&gt;
&lt;li&gt;Is Git history analyzed?&lt;/li&gt;
&lt;li&gt;Is IaC reviewed?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Validation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are findings manually confirmed?&lt;/li&gt;
&lt;li&gt;Is controlled exploitation performed?&lt;/li&gt;
&lt;li&gt;Are attack chains investigated?&lt;/li&gt;
&lt;li&gt;Is exploitability demonstrated safely?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Retesting&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is it included?&lt;/li&gt;
&lt;li&gt;How many cycles?&lt;/li&gt;
&lt;li&gt;Is production retested?&lt;/li&gt;
&lt;li&gt;Is a final retest report provided?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Evidence&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What reports are included?&lt;/li&gt;
&lt;li&gt;What compliance documentation ships with the engagement?&lt;/li&gt;
&lt;li&gt;How is sensitive test data stored?&lt;/li&gt;
&lt;li&gt;When is it deleted?&lt;/li&gt;
&lt;li&gt;Is deletion documented?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This turns a vague pricing comparison into an actual &lt;strong&gt;coverage comparison&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, Is the $22,000 Pentest Worth It?
&lt;/h2&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;The $22,000 test isn't automatically better.&lt;/p&gt;

&lt;p&gt;And the $6,500 test isn't automatically a waste of money.&lt;/p&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What risk does each engagement leave untested?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the $6,500 assessment covers only unauthenticated external scanning while the $22,000 engagement includes authenticated APIs, authorization testing, manual business logic testing, cloud assets, exploit validation, attack-path analysis, and retesting, you're not comparing two prices for the same product.&lt;/p&gt;

&lt;p&gt;You're comparing two different security assessments.&lt;/p&gt;

&lt;p&gt;That's the part of the pricing conversation that often gets lost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't compare the number at the top of the quote. Compare the attack surface, methodology, validation, evidence, and lifecycle coverage underneath it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How much does a penetration test cost in 2026?
&lt;/h3&gt;

&lt;p&gt;Planning ranges can start around $3,000 for limited external testing and exceed $100,000 for large integrated assessments. The actual price depends heavily on scope, application complexity, authenticated workflows, infrastructure, source-code access, methodology, and deliverables.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why can two pentest quotes differ by 3×?
&lt;/h3&gt;

&lt;p&gt;Because “penetration test” doesn't define a standardized amount of work. One quote may include automated scanning and limited validation, while another may include authenticated testing, source review, manual business logic testing, exploit validation, attack-path analysis, and retesting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a cheap penetration test useless?
&lt;/h3&gt;

&lt;p&gt;No. A limited assessment can provide useful baseline visibility. The important thing is understanding what it is designed to detect and what it explicitly doesn't cover.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should retesting be included?
&lt;/h3&gt;

&lt;p&gt;Ideally, yes, or at least the retesting process and cost should be explicit in the SOW. Otherwise, the initial quote may not represent the total cost of completing the testing lifecycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is black box, gray box, or white box testing best?
&lt;/h3&gt;

&lt;p&gt;None is universally “best.” Black box provides an outside-in view, gray box is particularly useful for authenticated authorization and workflow testing, and white box exposes implementation-level weaknesses. For high-risk applications, combining perspectives can provide much stronger coverage.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This piece is adapted from a longer breakdown on the CodeAnt AI blog, including a detailed quote-comparison framework and an analysis of alternative penetration testing pricing models.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pentesting</category>
      <category>vulnerabilities</category>
      <category>cybersecurity</category>
      <category>penetrationtesting</category>
    </item>
    <item>
      <title>External Penetration Testing in 2026: A Technical Methodology, Tool Stack, and Attack Surface Guide</title>
      <dc:creator>Amartya Jha</dc:creator>
      <pubDate>Wed, 09 Sep 2026 10:46:46 +0000</pubDate>
      <link>https://dev.to/codeant/external-penetration-testing-in-2026-a-technical-methodology-tool-stack-and-attack-surface-guide-10g1</link>
      <guid>https://dev.to/codeant/external-penetration-testing-in-2026-a-technical-methodology-tool-stack-and-attack-surface-guide-10g1</guid>
      <description>&lt;p&gt;External penetration testing is often reduced to a familiar sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nmap → nuclei → Burp → report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That workflow is useful, but it misses the hardest part of the engagement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finding the assets worth testing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A production application is rarely the entire external attack surface.&lt;/p&gt;

&lt;p&gt;There may be a staging environment on another subdomain, an old API version on a different hostname, a forgotten VPN portal, a cloud storage bucket, a CI/CD interface, an exposed database, or a service that was intended to be internal but is reachable from the internet.&lt;/p&gt;

&lt;p&gt;If the tester doesn't discover those assets, none of the vulnerability scanners matter.&lt;/p&gt;

&lt;p&gt;This guide presents a technical external penetration testing methodology from the perspective of an attacker starting with only a target organization and its public footprint.&lt;/p&gt;

&lt;p&gt;The focus is on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Passive reconnaissance&lt;/li&gt;
&lt;li&gt;DNS and certificate intelligence&lt;/li&gt;
&lt;li&gt;Attack-surface discovery&lt;/li&gt;
&lt;li&gt;Active host and port enumeration&lt;/li&gt;
&lt;li&gt;Web and API discovery&lt;/li&gt;
&lt;li&gt;Vulnerability identification&lt;/li&gt;
&lt;li&gt;Manual validation&lt;/li&gt;
&lt;li&gt;Safe exploitation&lt;/li&gt;
&lt;li&gt;Cloud exposure&lt;/li&gt;
&lt;li&gt;Commonly missed attack paths&lt;/li&gt;
&lt;li&gt;Tool selection&lt;/li&gt;
&lt;li&gt;Reporting evidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The commands below are intended for systems you are authorized to test. Production systems should be tested within the agreed scope, rate limits, and rules of engagement.&lt;/p&gt;




&lt;h1&gt;
  
  
  1. Define the Attack Surface Before Testing It
&lt;/h1&gt;

&lt;p&gt;The first mistake in external penetration testing is treating the client's domain as the attack surface.&lt;/p&gt;

&lt;p&gt;It isn't.&lt;/p&gt;

&lt;p&gt;The domain is the starting point.&lt;/p&gt;

&lt;p&gt;The actual attack surface can look more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;company.com
├── www.company.com
├── app.company.com
├── api.company.com
├── api-v2.company.com
├── staging.company.com
├── dev.company.com
├── vpn.company.com
├── sso.company.com
├── git.company.com
├── ci.company.com
├── monitoring.company.com
└── legacy.company.com

Cloud
├── public S3 buckets
├── public load balancers
├── exposed databases
├── Kubernetes APIs
└── forgotten public IPs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first objective is therefore:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Build an inventory of externally reachable assets before attempting to exploit them.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is where passive OSINT and attack-surface discovery become more important than simply running a vulnerability scanner.&lt;/p&gt;




&lt;h1&gt;
  
  
  2. Phase One: Passive Reconnaissance
&lt;/h1&gt;

&lt;p&gt;Passive reconnaissance gathers information without directly probing the target infrastructure.&lt;/p&gt;

&lt;p&gt;The goal is to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What domains belong to the organization?&lt;/li&gt;
&lt;li&gt;What IP ranges are associated with it?&lt;/li&gt;
&lt;li&gt;What subdomains exist?&lt;/li&gt;
&lt;li&gt;What technologies are publicly visible?&lt;/li&gt;
&lt;li&gt;What third-party services are being used?&lt;/li&gt;
&lt;li&gt;What infrastructure has historically existed?&lt;/li&gt;
&lt;li&gt;Have credentials or secrets appeared in public repositories?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2.1 DNS Enumeration
&lt;/h2&gt;

&lt;p&gt;Start with the basic DNS records.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short company.com A
dig +short company.com AAAA
dig +short company.com MX
dig +short company.com NS
dig +short company.com TXT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each record can provide a different piece of infrastructure intelligence.&lt;/p&gt;

&lt;h3&gt;
  
  
  A / AAAA
&lt;/h3&gt;

&lt;p&gt;These identify IPv4 and IPv6 addresses associated with the domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  MX
&lt;/h3&gt;

&lt;p&gt;Mail records can reveal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Microsoft 365&lt;/li&gt;
&lt;li&gt;Google Workspace&lt;/li&gt;
&lt;li&gt;third-party email providers&lt;/li&gt;
&lt;li&gt;dedicated mail infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  TXT
&lt;/h3&gt;

&lt;p&gt;TXT records commonly contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SPF configuration&lt;/li&gt;
&lt;li&gt;domain verification records&lt;/li&gt;
&lt;li&gt;SaaS integrations&lt;/li&gt;
&lt;li&gt;cloud-provider verification&lt;/li&gt;
&lt;li&gt;email security configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  NS
&lt;/h3&gt;

&lt;p&gt;Nameservers identify the DNS infrastructure responsible for the domain.&lt;/p&gt;




&lt;h1&gt;
  
  
  3. Test for DNS Zone Transfers
&lt;/h1&gt;

&lt;p&gt;A misconfigured authoritative DNS server may allow an AXFR request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig axfr company.com @ns1.company.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful response can disclose the entire DNS zone.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dev.company.com
staging.company.com
internal.company.com
db01.company.com
vpn.company.com
old-api.company.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A failed AXFR is expected.&lt;/p&gt;

&lt;p&gt;A successful transfer is materially different because it can expose internal naming conventions and infrastructure that may not appear through ordinary enumeration.&lt;/p&gt;




&lt;h1&gt;
  
  
  4. Certificate Transparency
&lt;/h1&gt;

&lt;p&gt;Certificate Transparency logs are one of the most useful passive sources for discovering forgotten hostnames.&lt;/p&gt;

&lt;p&gt;A simple &lt;code&gt;crt.sh&lt;/code&gt; query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://crt.sh/?q=%25.company.com&amp;amp;output=json"&lt;/span&gt; |
    jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[].name_value'&lt;/span&gt; |
    &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normalize the results before continuing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://crt.sh/?q=%25.company.com&amp;amp;output=json"&lt;/span&gt; |
    jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[].name_value'&lt;/span&gt; |
    &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/\*\.//g'&lt;/span&gt; |
    &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Certificate data can reveal hosts that aren't linked from the organization's main website.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.company.com
api.company.com
staging.company.com
qa.company.com
legacy.company.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important point is that &lt;strong&gt;discovery is not vulnerability confirmation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;staging.company.com&lt;/code&gt; isn't a finding simply because it exists.&lt;/p&gt;

&lt;p&gt;It is an asset that now needs testing.&lt;/p&gt;




&lt;h1&gt;
  
  
  5. ASN and IP Range Discovery
&lt;/h1&gt;

&lt;p&gt;If the engagement includes network infrastructure, determine which public ranges are associated with the organization.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;whois &lt;span class="nt"&gt;-h&lt;/span&gt; whois.radb.net &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'-i origin AS12345'&lt;/span&gt; |
    &lt;span class="nb"&gt;grep &lt;/span&gt;route:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ASN information can reveal infrastructure that isn't directly associated with the primary domain.&lt;/p&gt;

&lt;p&gt;You can then use the resulting ranges as input to the active reconnaissance phase.&lt;/p&gt;

&lt;p&gt;This is particularly useful for organizations operating their own infrastructure rather than relying entirely on cloud providers.&lt;/p&gt;




&lt;h1&gt;
  
  
  6. Historical URL Discovery
&lt;/h1&gt;

&lt;p&gt;Current crawling only tells you what exists now.&lt;/p&gt;

&lt;p&gt;Historical sources can reveal endpoints that existed previously.&lt;/p&gt;

&lt;p&gt;Two common tools are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gau company.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;waybackurls company.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then filter for interesting paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gau company.com |
    &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Ei&lt;/span&gt; &lt;span class="s1"&gt;'\.(json|xml|config|bak|sql|zip|env)$'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or search for administrative functionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gau company.com |
    &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Ei&lt;/span&gt; &lt;span class="s1"&gt;'(admin|debug|internal|api|graphql|swagger)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Historical URLs are particularly useful because applications evolve.&lt;/p&gt;

&lt;p&gt;An endpoint may disappear from the current interface while remaining deployed.&lt;/p&gt;




&lt;h1&gt;
  
  
  7. Public Repository Reconnaissance
&lt;/h1&gt;

&lt;p&gt;Public source repositories can expose much more than source code.&lt;/p&gt;

&lt;p&gt;Search for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API keys&lt;/li&gt;
&lt;li&gt;cloud credentials&lt;/li&gt;
&lt;li&gt;internal hostnames&lt;/li&gt;
&lt;li&gt;database URLs&lt;/li&gt;
&lt;li&gt;deployment scripts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;private package registries&lt;/li&gt;
&lt;li&gt;CI/CD configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"company.com" API_KEY
"company.com" AWS_SECRET
"company-internal"
"database.company.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Secret scanning tools can help automate this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;trufflehog github &lt;span class="nt"&gt;--org&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;company
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A discovered credential still needs validation.&lt;/p&gt;

&lt;p&gt;Don't assume that every string matching &lt;code&gt;AWS_SECRET&lt;/code&gt; is an active credential.&lt;/p&gt;

&lt;p&gt;The tester should establish:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is it syntactically valid?&lt;/li&gt;
&lt;li&gt;Is it still active?&lt;/li&gt;
&lt;li&gt;What identity does it belong to?&lt;/li&gt;
&lt;li&gt;What permissions does it have?&lt;/li&gt;
&lt;li&gt;Does it provide access to in-scope resources?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That turns a potential secret leak into a measurable security finding.&lt;/p&gt;




&lt;h1&gt;
  
  
  8. Build the Initial Asset Inventory
&lt;/h1&gt;

&lt;p&gt;At this stage, consolidate the passive results.&lt;/p&gt;

&lt;p&gt;A useful working inventory might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hostname                  IP              source
----------------------------------------------------------
www.company.com           203.0.113.10    DNS
api.company.com           203.0.113.20    CT
staging.company.com       203.0.113.30    CT
vpn.company.com           203.0.113.40    DNS
legacy.company.com        203.0.113.50    Wayback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't immediately start exploiting.&lt;/p&gt;

&lt;p&gt;First remove:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicates&lt;/li&gt;
&lt;li&gt;third-party assets outside scope&lt;/li&gt;
&lt;li&gt;CDN infrastructure&lt;/li&gt;
&lt;li&gt;unrelated shared hosting&lt;/li&gt;
&lt;li&gt;dead DNS records&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then resolve the remaining hosts.&lt;/p&gt;




&lt;h1&gt;
  
  
  9. Phase Two: Active Reconnaissance
&lt;/h1&gt;

&lt;p&gt;Now the tester starts interacting directly with the target.&lt;/p&gt;

&lt;p&gt;The objective is to determine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which hosts are alive?&lt;/li&gt;
&lt;li&gt;Which ports are open?&lt;/li&gt;
&lt;li&gt;What services are running?&lt;/li&gt;
&lt;li&gt;Which applications are exposed?&lt;/li&gt;
&lt;li&gt;Which technologies are being used?&lt;/li&gt;
&lt;li&gt;Which endpoints exist?&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  10. Subdomain Enumeration
&lt;/h1&gt;

&lt;p&gt;Use multiple discovery sources where appropriate.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;subfinder &lt;span class="nt"&gt;-d&lt;/span&gt; company.com &lt;span class="nt"&gt;-silent&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; subdomains.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Amass can provide additional enumeration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;amass enum &lt;span class="nt"&gt;-passive&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; company.com &lt;span class="nt"&gt;-o&lt;/span&gt; amass.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merge the results:&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;cat &lt;/span&gt;subdomains.txt amass.txt |
    &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; |
    &lt;span class="nb"&gt;tee &lt;/span&gt;all_subdomains.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then resolve and probe them:&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;cat &lt;/span&gt;all_subdomains.txt |
    httpx &lt;span class="nt"&gt;-silent&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-status-code&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-title&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-tech-detect&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-follow-redirects&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-o&lt;/span&gt; live_hosts.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the tester has something much more useful than a raw subdomain list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://app.company.com       200   Customer Portal
https://api.company.com       200   API
https://staging.company.com   200   Staging
https://vpn.company.com       302   VPN Login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  11. Full Port Enumeration
&lt;/h1&gt;

&lt;p&gt;For identified IP addresses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-p-&lt;/span&gt; &lt;span class="nt"&gt;--open&lt;/span&gt; &lt;span class="nt"&gt;-sV&lt;/span&gt; &lt;span class="nt"&gt;-sC&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-oA&lt;/span&gt; nmap_full_scan &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt;TARGET_IP]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For large ranges, scanners such as RustScan can accelerate initial port discovery before handing results to Nmap for service detection.&lt;/p&gt;

&lt;p&gt;The important distinction is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Port discovery tells you where something is listening.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service enumeration tells you what it is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;22/tcp    SSH
80/tcp    HTTP
443/tcp   HTTPS
3306/tcp  MySQL
6379/tcp  Redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each one becomes a separate testing path.&lt;/p&gt;




&lt;h1&gt;
  
  
  12. Prioritize the Interesting Ports
&lt;/h1&gt;

&lt;p&gt;Not every open port deserves equal attention.&lt;/p&gt;

&lt;p&gt;High-interest services include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;21      FTP
22      SSH
23      Telnet
25      SMTP
53      DNS
80      HTTP
443     HTTPS
445     SMB
1433    MSSQL
3306    MySQL
3389    RDP
5432    PostgreSQL
5900    VNC
6379    Redis
9200    Elasticsearch
2375    Docker API
6443    Kubernetes API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But don't turn this list into a severity checklist.&lt;/p&gt;

&lt;p&gt;An exposed port is an &lt;strong&gt;observation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The actual finding depends on what is accessible through it.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5432/tcp open PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not equivalent to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5432/tcp open PostgreSQL
Authentication disabled
Unauthenticated database access confirmed
Production data accessible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second is a security finding.&lt;/p&gt;




&lt;h1&gt;
  
  
  13. Web Technology Fingerprinting
&lt;/h1&gt;

&lt;p&gt;For HTTP services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;httpx &lt;span class="nt"&gt;-u&lt;/span&gt; https://company.com &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-status-code&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-title&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-tech-detect&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-server&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-content-length&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;framework fingerprints&lt;/li&gt;
&lt;li&gt;server versions&lt;/li&gt;
&lt;li&gt;exposed headers&lt;/li&gt;
&lt;li&gt;reverse proxies&lt;/li&gt;
&lt;li&gt;CDNs&lt;/li&gt;
&lt;li&gt;application frameworks&lt;/li&gt;
&lt;li&gt;CMS platforms&lt;/li&gt;
&lt;li&gt;API technologies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Technology identification can influence the next stage of testing.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx
PHP
Laravel
GraphQL
WordPress
Spring Boot
ASP.NET
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;each suggests a different set of likely endpoints and vulnerabilities.&lt;/p&gt;




&lt;h1&gt;
  
  
  14. Directory and Endpoint Discovery
&lt;/h1&gt;

&lt;p&gt;Once an application is identified, enumerate paths.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffuf &lt;span class="nt"&gt;-u&lt;/span&gt; https://company.com/FUZZ &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-w&lt;/span&gt; /usr/share/wordlists/dirb/common.txt &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-mc&lt;/span&gt; 200,204,301,302,307,401,403
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For extensions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffuf &lt;span class="nt"&gt;-u&lt;/span&gt; https://company.com/FUZZ &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-w&lt;/span&gt; wordlist.txt &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; .php,.json,.xml,.txt,.bak,.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interesting responses include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;200 OK
401 Unauthorized
403 Forbidden
500 Internal Server Error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;403&lt;/code&gt; can be interesting because it proves the endpoint exists.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;500&lt;/code&gt; can also be useful because error handling sometimes exposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stack traces&lt;/li&gt;
&lt;li&gt;framework versions&lt;/li&gt;
&lt;li&gt;database errors&lt;/li&gt;
&lt;li&gt;internal paths&lt;/li&gt;
&lt;li&gt;debug information&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  15. API Enumeration
&lt;/h1&gt;

&lt;p&gt;Modern external attack surfaces are increasingly API-heavy.&lt;/p&gt;

&lt;p&gt;Start by identifying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/api
/api/v1
/api/v2
/graphql
/swagger
/openapi.json
/api-docs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Search historical URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gau company.com |
    &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Ei&lt;/span&gt; &lt;span class="s1"&gt;'/api/|graphql|swagger|openapi'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an OpenAPI specification is publicly accessible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.company.com/openapi.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may immediately obtain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;endpoint names&lt;/li&gt;
&lt;li&gt;parameters&lt;/li&gt;
&lt;li&gt;object identifiers&lt;/li&gt;
&lt;li&gt;authentication schemes&lt;/li&gt;
&lt;li&gt;request methods&lt;/li&gt;
&lt;li&gt;API versions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This can dramatically improve manual testing.&lt;/p&gt;




&lt;h1&gt;
  
  
  16. Authentication Testing
&lt;/h1&gt;

&lt;p&gt;External authentication testing should go beyond:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Does the login page exist?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Test the complete authentication boundary.&lt;/p&gt;

&lt;p&gt;Questions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is MFA enforced?&lt;/li&gt;
&lt;li&gt;Can MFA be bypassed?&lt;/li&gt;
&lt;li&gt;Are password-reset tokens predictable?&lt;/li&gt;
&lt;li&gt;Do reset links expire?&lt;/li&gt;
&lt;li&gt;Can sessions be reused?&lt;/li&gt;
&lt;li&gt;Are tokens invalidated after logout?&lt;/li&gt;
&lt;li&gt;Are API tokens scoped?&lt;/li&gt;
&lt;li&gt;Are legacy authentication endpoints still active?&lt;/li&gt;
&lt;li&gt;Do mobile and web authentication paths behave differently?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common mistake is testing only the primary web login.&lt;/p&gt;

&lt;p&gt;The real authentication surface may include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/web/login
/api/login
/mobile/auth
/oauth/token
/sso/login
/password/reset
/admin/login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  17. Authorization Testing
&lt;/h1&gt;

&lt;p&gt;Authorization flaws are often more valuable than generic version-based findings because they depend on application logic.&lt;/p&gt;

&lt;p&gt;Consider an API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/users/1001
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change the identifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/users/1002
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If user 1001 can access user 1002's information, the issue is an Insecure Direct Object Reference or broader broken object-level authorization problem.&lt;/p&gt;

&lt;p&gt;The same principle applies to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/orders/1001
/invoices/1001
/projects/1001
/files/1001
/payments/1001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tester should ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does the server verify that this authenticated principal is actually authorized to access this object?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Changing an ID and receiving a &lt;code&gt;200&lt;/code&gt; response isn't enough by itself.&lt;/p&gt;

&lt;p&gt;The response needs to demonstrate unauthorized access.&lt;/p&gt;




&lt;h1&gt;
  
  
  18. Test Alternate API Versions
&lt;/h1&gt;

&lt;p&gt;One of the easiest blind spots is assuming that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/api/v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;has the same security controls as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/api/v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test both.&lt;/p&gt;

&lt;p&gt;Look for differences in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;authorization&lt;/li&gt;
&lt;li&gt;input validation&lt;/li&gt;
&lt;li&gt;rate limiting&lt;/li&gt;
&lt;li&gt;object-level access controls&lt;/li&gt;
&lt;li&gt;error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An old API doesn't need to be linked from the frontend to remain exploitable.&lt;/p&gt;

&lt;p&gt;If it is internet-accessible, it is part of the attack surface.&lt;/p&gt;




&lt;h1&gt;
  
  
  19. Vulnerability Scanning
&lt;/h1&gt;

&lt;p&gt;Once the attack surface is mapped, automated vulnerability detection becomes much more useful.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nuclei &lt;span class="nt"&gt;-l&lt;/span&gt; live_hosts.txt &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-severity&lt;/span&gt; critical,high,medium &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-tags&lt;/span&gt; cve,exposure,misconfig &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-o&lt;/span&gt; nuclei.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run scanners against known technology where possible rather than blindly scanning everything.&lt;/p&gt;

&lt;p&gt;The output should become a triage queue.&lt;/p&gt;

&lt;p&gt;Not the final report.&lt;/p&gt;




&lt;h1&gt;
  
  
  20. Scanner Finding vs Real Finding
&lt;/h1&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Nuclei:
Apache CVE detected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a lead.&lt;/p&gt;

&lt;p&gt;The validation workflow should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scanner match
     ↓
Identify exact version
     ↓
Confirm affected component
     ↓
Determine whether vulnerable feature is reachable
     ↓
Reproduce safely
     ↓
Establish impact
     ↓
Collect evidence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only then should it become a confirmed finding.&lt;/p&gt;

&lt;p&gt;This matters because scanners can produce false positives from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backported patches&lt;/li&gt;
&lt;li&gt;incorrect version detection&lt;/li&gt;
&lt;li&gt;reverse proxies&lt;/li&gt;
&lt;li&gt;custom builds&lt;/li&gt;
&lt;li&gt;disabled vulnerable modules&lt;/li&gt;
&lt;li&gt;unreachable vulnerable functionality&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  21. Validate Exposed Redis
&lt;/h1&gt;

&lt;p&gt;If Redis is exposed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-cli &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;TARGET_IP] ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A response of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PONG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;shows connectivity.&lt;/p&gt;

&lt;p&gt;It doesn't automatically prove unauthenticated administrative access.&lt;/p&gt;

&lt;p&gt;The next question is whether commands requiring authentication are permitted.&lt;/p&gt;

&lt;p&gt;For example, safely establish the access level permitted under the engagement rules.&lt;/p&gt;

&lt;p&gt;The finding should ultimately describe &lt;strong&gt;what access was demonstrated&lt;/strong&gt;, not merely that port 6379 was open.&lt;/p&gt;




&lt;h1&gt;
  
  
  22. Validate Database Exposure
&lt;/h1&gt;

&lt;p&gt;For a PostgreSQL service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;TARGET_IP] &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;TEST_USER] &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;DATABASE]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For MySQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;TARGET_IP] &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;TEST_USER] &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The purpose is to establish whether:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication is required&lt;/li&gt;
&lt;li&gt;credentials work&lt;/li&gt;
&lt;li&gt;access is restricted&lt;/li&gt;
&lt;li&gt;the exposed account has excessive privileges&lt;/li&gt;
&lt;li&gt;sensitive data is accessible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Never dump unnecessary production data simply to prove access.&lt;/p&gt;

&lt;p&gt;A small, controlled proof is generally enough.&lt;/p&gt;




&lt;h1&gt;
  
  
  23. Exposed Management Interfaces
&lt;/h1&gt;

&lt;p&gt;Management systems deserve special attention because they often have significantly more privileges than ordinary applications.&lt;/p&gt;

&lt;p&gt;Common examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Jenkins
Grafana
Kibana
Argo CD
Rancher
GitLab
Prometheus
Docker
Kubernetes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tester should determine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the interface publicly accessible?&lt;/li&gt;
&lt;li&gt;Is authentication required?&lt;/li&gt;
&lt;li&gt;What identity is created after authentication?&lt;/li&gt;
&lt;li&gt;What actions can that identity perform?&lt;/li&gt;
&lt;li&gt;Can the interface reach internal infrastructure?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A publicly reachable Jenkins login page isn't automatically a vulnerability.&lt;/p&gt;

&lt;p&gt;An unauthenticated Jenkins instance allowing job execution is an entirely different problem.&lt;/p&gt;




&lt;h1&gt;
  
  
  24. Cloud Attack Surface
&lt;/h1&gt;

&lt;p&gt;External testing increasingly means testing cloud infrastructure rather than traditional perimeter devices.&lt;/p&gt;

&lt;p&gt;Common areas include:&lt;/p&gt;

&lt;h3&gt;
  
  
  Object storage
&lt;/h3&gt;

&lt;p&gt;Check for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;public listing&lt;/li&gt;
&lt;li&gt;public reads&lt;/li&gt;
&lt;li&gt;public writes&lt;/li&gt;
&lt;li&gt;unintended object exposure&lt;/li&gt;
&lt;li&gt;backup files&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Public load balancers
&lt;/h3&gt;

&lt;p&gt;Identify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backend services&lt;/li&gt;
&lt;li&gt;alternate listeners&lt;/li&gt;
&lt;li&gt;forgotten ports&lt;/li&gt;
&lt;li&gt;administrative endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cloud-hosted databases
&lt;/h3&gt;

&lt;p&gt;Look for publicly reachable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RDS
Cloud SQL
MongoDB
Redis
Elasticsearch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Kubernetes
&lt;/h3&gt;

&lt;p&gt;Look for exposed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Kubernetes API
Dashboard
Ingress controllers
Metrics
Management interfaces
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloud security failures often come from resources that were created temporarily and never removed.&lt;/p&gt;




&lt;h1&gt;
  
  
  25. Public S3 Bucket Discovery
&lt;/h1&gt;

&lt;p&gt;Bucket discovery can begin with known naming patterns, but DNS and application source code can provide better candidates.&lt;/p&gt;

&lt;p&gt;For a bucket you are authorized to test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 &lt;span class="nb"&gt;ls &lt;/span&gt;s3://bucket-name &lt;span class="nt"&gt;--no-sign-request&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful listing demonstrates public listing access.&lt;/p&gt;

&lt;p&gt;Then determine whether objects are publicly readable.&lt;/p&gt;

&lt;p&gt;Do not assume that because a bucket exists, all of its contents are public.&lt;/p&gt;

&lt;p&gt;Test the actual permission boundary.&lt;/p&gt;

&lt;p&gt;The distinction between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bucket exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;anonymous principal can list and download objects
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is the difference between asset discovery and a confirmed exposure.&lt;/p&gt;




&lt;h1&gt;
  
  
  26. Exposed &lt;code&gt;.git&lt;/code&gt; Directories
&lt;/h1&gt;

&lt;p&gt;A public &lt;code&gt;.git&lt;/code&gt; directory can potentially expose repository history.&lt;/p&gt;

&lt;p&gt;Check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://company.com/.git/HEAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If accessible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://company.com/.git/HEAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on what is exposed, an attacker may be able to reconstruct repository information.&lt;/p&gt;

&lt;p&gt;The security impact depends on what the repository contains.&lt;/p&gt;

&lt;p&gt;Potentially sensitive material includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source code&lt;/li&gt;
&lt;li&gt;credentials&lt;/li&gt;
&lt;li&gt;deployment configuration&lt;/li&gt;
&lt;li&gt;internal endpoints&lt;/li&gt;
&lt;li&gt;historical secrets&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  27. Exposed &lt;code&gt;.env&lt;/code&gt; Files
&lt;/h1&gt;

&lt;p&gt;A simple check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; https://company.com/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A response containing environment configuration can be severe if it exposes active credentials.&lt;/p&gt;

&lt;p&gt;Potential entries include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_HOST=
DB_USERNAME=
DB_PASSWORD=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
STRIPE_SECRET_KEY=
JWT_SECRET=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, the key question is whether the values are actually active and what access they provide.&lt;/p&gt;

&lt;p&gt;A leaked secret should be validated carefully and reported with enough evidence to demonstrate impact without unnecessarily exposing the secret itself.&lt;/p&gt;




&lt;h1&gt;
  
  
  28. Subdomain Takeover Testing
&lt;/h1&gt;

&lt;p&gt;A typical workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;subdomain discovered
        ↓
DNS record inspected
        ↓
CNAME points to third-party service
        ↓
resource no longer exists
        ↓
service confirms resource can potentially be claimed
        ↓
controlled validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A dangling CNAME alone is not sufficient evidence.&lt;/p&gt;

&lt;p&gt;The tester needs to establish whether the third-party resource is actually claimable.&lt;/p&gt;




&lt;h1&gt;
  
  
  29. The Findings Automation Commonly Misses
&lt;/h1&gt;

&lt;p&gt;The most interesting external pentest findings aren't always CVEs.&lt;/p&gt;

&lt;p&gt;They are often inconsistencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Different authorization between UI and API
&lt;/h3&gt;

&lt;p&gt;The UI checks permission A.&lt;/p&gt;

&lt;p&gt;The API checks permission B.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production and staging have different security controls
&lt;/h3&gt;

&lt;p&gt;Production requires MFA.&lt;/p&gt;

&lt;p&gt;Staging doesn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  API v1 and v2 implement authorization differently
&lt;/h3&gt;

&lt;p&gt;One version checks object ownership.&lt;/p&gt;

&lt;p&gt;The other trusts the object ID supplied by the client.&lt;/p&gt;

&lt;h3&gt;
  
  
  Legacy endpoints remain accessible
&lt;/h3&gt;

&lt;p&gt;The frontend stopped using &lt;code&gt;/api/v1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The server never stopped serving it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Forgotten infrastructure has stronger connectivity than expected
&lt;/h3&gt;

&lt;p&gt;A staging server is internet-facing but also has access to internal production services.&lt;/p&gt;

&lt;p&gt;These findings require understanding &lt;strong&gt;relationships&lt;/strong&gt;, not just signatures.&lt;/p&gt;

&lt;p&gt;That's why manual testing remains essential even when automated scanning is extensive.&lt;/p&gt;




&lt;h1&gt;
  
  
  30. Attack-Path Thinking
&lt;/h1&gt;

&lt;p&gt;The strongest pentests don't treat findings as isolated rows in a spreadsheet.&lt;/p&gt;

&lt;p&gt;They ask whether findings can be chained.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Forgotten subdomain
        ↓
Staging application
        ↓
Debug endpoint
        ↓
Cloud credential exposure
        ↓
Overprivileged IAM identity
        ↓
Production storage access
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of the individual observations necessarily represents the complete impact.&lt;/p&gt;

&lt;p&gt;The chain does.&lt;/p&gt;

&lt;p&gt;Another example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exposed management interface
        ↓
Weak authentication
        ↓
Administrative access
        ↓
CI/CD job execution
        ↓
Cloud credentials
        ↓
Production environment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why external penetration testing is different from simply producing a vulnerability scan.&lt;/p&gt;

&lt;p&gt;The tester is trying to understand &lt;strong&gt;what an attacker can do with the access they obtain&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  31. External Pentesting Tool Stack
&lt;/h1&gt;

&lt;p&gt;A practical toolchain might look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dig&lt;/code&gt;, &lt;code&gt;dnsx&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;DNS enumeration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subdomains&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;subfinder&lt;/code&gt;, &lt;code&gt;Amass&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Host discovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certificate data&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crt.sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Passive hostname discovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP probing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;httpx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Live host identification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port scanning&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Nmap&lt;/code&gt;, RustScan&lt;/td&gt;
&lt;td&gt;Port and service discovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web discovery&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ffuf&lt;/code&gt;, Gobuster&lt;/td&gt;
&lt;td&gt;Endpoint enumeration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historical discovery&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gau&lt;/code&gt;, &lt;code&gt;waybackurls&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Old endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSINT&lt;/td&gt;
&lt;td&gt;Shodan, Censys, theHarvester&lt;/td&gt;
&lt;td&gt;Public infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vulnerability scanning&lt;/td&gt;
&lt;td&gt;Nuclei&lt;/td&gt;
&lt;td&gt;CVEs and misconfigurations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret detection&lt;/td&gt;
&lt;td&gt;TruffleHog&lt;/td&gt;
&lt;td&gt;Public credential discovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web testing&lt;/td&gt;
&lt;td&gt;Burp Suite&lt;/td&gt;
&lt;td&gt;Manual HTTP testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exploitation&lt;/td&gt;
&lt;td&gt;Metasploit&lt;/td&gt;
&lt;td&gt;Controlled exploitation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;AWS/Azure/GCP CLI tools&lt;/td&gt;
&lt;td&gt;Cloud validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMB/Windows&lt;/td&gt;
&lt;td&gt;enum4linux&lt;/td&gt;
&lt;td&gt;SMB enumeration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The exact stack should change according to the target.&lt;/p&gt;

&lt;p&gt;A SaaS application doesn't need the same workflow as an enterprise network containing VPN concentrators, Windows infrastructure, and exposed management systems.&lt;/p&gt;




&lt;h1&gt;
  
  
  32. External vs Internal Penetration Testing
&lt;/h1&gt;

&lt;p&gt;These engagements begin from fundamentally different trust assumptions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;External&lt;/th&gt;
&lt;th&gt;Internal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Starting point&lt;/td&gt;
&lt;td&gt;Internet&lt;/td&gt;
&lt;td&gt;Internal network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credentials&lt;/td&gt;
&lt;td&gt;Usually none&lt;/td&gt;
&lt;td&gt;Usually provided or obtained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recon&lt;/td&gt;
&lt;td&gt;OSINT, DNS, public infrastructure&lt;/td&gt;
&lt;td&gt;Network and directory enumeration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary target&lt;/td&gt;
&lt;td&gt;Perimeter&lt;/td&gt;
&lt;td&gt;Internal systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical findings&lt;/td&gt;
&lt;td&gt;Exposed services, web/API flaws&lt;/td&gt;
&lt;td&gt;AD, privilege escalation, lateral movement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main question&lt;/td&gt;
&lt;td&gt;Can an attacker get in?&lt;/td&gt;
&lt;td&gt;What can they do after getting in?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An external pentest shouldn't be treated as a cheaper version of an internal pentest.&lt;/p&gt;

&lt;p&gt;They model different attack positions.&lt;/p&gt;




&lt;h1&gt;
  
  
  33. Reporting a Finding Properly
&lt;/h1&gt;

&lt;p&gt;A useful finding should answer five questions:&lt;/p&gt;

&lt;h3&gt;
  
  
  What is vulnerable?
&lt;/h3&gt;

&lt;p&gt;Identify the exact asset and component.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where is it?
&lt;/h3&gt;

&lt;p&gt;Provide the hostname, endpoint, port, or resource.&lt;/p&gt;

&lt;h3&gt;
  
  
  How was it validated?
&lt;/h3&gt;

&lt;p&gt;Give a reproducible proof.&lt;/p&gt;

&lt;h3&gt;
  
  
  What can an attacker do?
&lt;/h3&gt;

&lt;p&gt;Explain the actual impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  How should it be fixed?
&lt;/h3&gt;

&lt;p&gt;Provide a practical remediation.&lt;/p&gt;

&lt;p&gt;For example, instead of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Redis exposed to internet — Critical&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;write:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Unauthenticated Redis instance accessible from the public internet&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;redis.company.com:6379&lt;/code&gt; accepts unauthenticated commands from an external network. During validation, the tester was able to enumerate the accessible Redis environment without credentials. Network exposure should be restricted to trusted application hosts and authentication should be enforced.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is substantially more useful.&lt;/p&gt;




&lt;h1&gt;
  
  
  34. Evidence Matters
&lt;/h1&gt;

&lt;p&gt;Good evidence should allow another engineer to reproduce the finding.&lt;/p&gt;

&lt;p&gt;Useful evidence includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request
Response
Hostname
Port
Timestamp
Authenticated identity
Relevant configuration
Minimal proof of impact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid collecting unnecessary sensitive data.&lt;/p&gt;

&lt;p&gt;For example, if a database contains 10 million customer records, you don't need to export all 10 million records to prove unauthorized database access.&lt;/p&gt;

&lt;p&gt;A single authorized test record or metadata response may be enough.&lt;/p&gt;




&lt;h1&gt;
  
  
  35. A Practical External Pentest Workflow
&lt;/h1&gt;

&lt;p&gt;Putting everything together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 TARGET
                    │
                    ▼
             Passive OSINT
                    │
          ┌─────────┼─────────┐
          ▼         ▼         ▼
         DNS       CT       ASN/IP
          │         │         │
          └─────────┼─────────┘
                    ▼
             Asset Inventory
                    │
                    ▼
          Active Reconnaissance
                    │
          ┌─────────┼─────────┐
          ▼         ▼         ▼
       HTTP       Ports      APIs
          │         │         │
          └─────────┼─────────┘
                    ▼
          Vulnerability Testing
                    │
                    ▼
             Manual Validation
                    │
                    ▼
          Controlled Exploitation
                    │
                    ▼
             Attack-Path Analysis
                    │
                    ▼
                Reporting
                    │
                    ▼
                Retesting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is that each phase informs the next.&lt;/p&gt;

&lt;p&gt;Certificate Transparency discovers a hostname.&lt;/p&gt;

&lt;p&gt;The hostname resolves to an IP.&lt;/p&gt;

&lt;p&gt;The IP exposes a service.&lt;/p&gt;

&lt;p&gt;The service identifies an application.&lt;/p&gt;

&lt;p&gt;The application exposes an API.&lt;/p&gt;

&lt;p&gt;The API contains an authorization flaw.&lt;/p&gt;

&lt;p&gt;The authorization flaw provides access to another object.&lt;/p&gt;

&lt;p&gt;That is the attack path.&lt;/p&gt;




&lt;h1&gt;
  
  
  36. What a Good External Pentest Actually Proves
&lt;/h1&gt;

&lt;p&gt;At the end of the engagement, the important question isn't:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How many vulnerabilities did we find?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What could an external attacker actually accomplish?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A good external penetration test should establish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What infrastructure is publicly reachable&lt;/li&gt;
&lt;li&gt;What applications are exposed&lt;/li&gt;
&lt;li&gt;Which services are vulnerable&lt;/li&gt;
&lt;li&gt;Which authentication controls can be bypassed&lt;/li&gt;
&lt;li&gt;Which authorization boundaries fail&lt;/li&gt;
&lt;li&gt;Which sensitive resources are accessible&lt;/li&gt;
&lt;li&gt;Which findings can be chained&lt;/li&gt;
&lt;li&gt;What level of access an attacker can obtain&lt;/li&gt;
&lt;li&gt;What remediation actually closes the attack path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ten validated findings can be more valuable than 500 scanner alerts.&lt;/p&gt;




&lt;h1&gt;
  
  
  37. External Penetration Testing in a Continuously Changing Environment
&lt;/h1&gt;

&lt;p&gt;The traditional model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pentest
   ↓
Report
   ↓
Fix
   ↓
Retest
   ↓
Wait
   ↓
Next annual pentest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is the environment changes during the waiting period.&lt;/p&gt;

&lt;p&gt;A new hostname can appear tomorrow.&lt;/p&gt;

&lt;p&gt;A new API can be deployed next week.&lt;/p&gt;

&lt;p&gt;A cloud resource can become public after a configuration change.&lt;/p&gt;

&lt;p&gt;A development environment can be exposed without ever entering the original pentest scope.&lt;/p&gt;

&lt;p&gt;This is why external attack-surface monitoring is increasingly complementary to periodic penetration testing.&lt;/p&gt;

&lt;p&gt;The methodology doesn't fundamentally change.&lt;/p&gt;

&lt;p&gt;The cadence does.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;discover → test → report → wait
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the model becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;discover → test → validate → remediate → retest → repeat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CodeAnt AI's analysis of &lt;a href="https://codeant.ai/blogs/continuous-vs-annual-pentesting" rel="noopener noreferrer"&gt;continuous vs. annual penetration testing&lt;/a&gt; goes deeper into this difference in testing cadence.&lt;/p&gt;




&lt;h1&gt;
  
  
  38. External Penetration Testing Checklist
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Reconnaissance
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] DNS records&lt;/li&gt;
&lt;li&gt;[ ] Zone transfer&lt;/li&gt;
&lt;li&gt;[ ] Certificate Transparency&lt;/li&gt;
&lt;li&gt;[ ] ASN discovery&lt;/li&gt;
&lt;li&gt;[ ] IP ranges&lt;/li&gt;
&lt;li&gt;[ ] Public repositories&lt;/li&gt;
&lt;li&gt;[ ] Secret exposure&lt;/li&gt;
&lt;li&gt;[ ] Historical URLs&lt;/li&gt;
&lt;li&gt;[ ] Search-engine indexing&lt;/li&gt;
&lt;li&gt;[ ] Public cloud resources&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Attack Surface
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Subdomains&lt;/li&gt;
&lt;li&gt;[ ] Live hosts&lt;/li&gt;
&lt;li&gt;[ ] IPv4&lt;/li&gt;
&lt;li&gt;[ ] IPv6&lt;/li&gt;
&lt;li&gt;[ ] Open ports&lt;/li&gt;
&lt;li&gt;[ ] Service versions&lt;/li&gt;
&lt;li&gt;[ ] Web technologies&lt;/li&gt;
&lt;li&gt;[ ] APIs&lt;/li&gt;
&lt;li&gt;[ ] Staging environments&lt;/li&gt;
&lt;li&gt;[ ] Legacy applications&lt;/li&gt;
&lt;li&gt;[ ] Management interfaces&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Web/API
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Authentication&lt;/li&gt;
&lt;li&gt;[ ] MFA&lt;/li&gt;
&lt;li&gt;[ ] Password reset&lt;/li&gt;
&lt;li&gt;[ ] Session management&lt;/li&gt;
&lt;li&gt;[ ] Authorization&lt;/li&gt;
&lt;li&gt;[ ] IDOR/BOLA&lt;/li&gt;
&lt;li&gt;[ ] API versioning&lt;/li&gt;
&lt;li&gt;[ ] GraphQL&lt;/li&gt;
&lt;li&gt;[ ] File upload&lt;/li&gt;
&lt;li&gt;[ ] Debug endpoints&lt;/li&gt;
&lt;li&gt;[ ] Sensitive files&lt;/li&gt;
&lt;li&gt;[ ] Security headers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Infrastructure
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] RDP&lt;/li&gt;
&lt;li&gt;[ ] VNC&lt;/li&gt;
&lt;li&gt;[ ] SSH&lt;/li&gt;
&lt;li&gt;[ ] SMB&lt;/li&gt;
&lt;li&gt;[ ] FTP&lt;/li&gt;
&lt;li&gt;[ ] Databases&lt;/li&gt;
&lt;li&gt;[ ] Redis&lt;/li&gt;
&lt;li&gt;[ ] Elasticsearch&lt;/li&gt;
&lt;li&gt;[ ] Docker API&lt;/li&gt;
&lt;li&gt;[ ] Kubernetes API&lt;/li&gt;
&lt;li&gt;[ ] VPN&lt;/li&gt;
&lt;li&gt;[ ] CI/CD&lt;/li&gt;
&lt;li&gt;[ ] Monitoring interfaces&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cloud
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Public buckets&lt;/li&gt;
&lt;li&gt;[ ] Public databases&lt;/li&gt;
&lt;li&gt;[ ] Public management interfaces&lt;/li&gt;
&lt;li&gt;[ ] Exposed credentials&lt;/li&gt;
&lt;li&gt;[ ] IAM permissions&lt;/li&gt;
&lt;li&gt;[ ] Forgotten resources&lt;/li&gt;
&lt;li&gt;[ ] Cloud metadata exposure&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Validation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Scanner findings manually validated&lt;/li&gt;
&lt;li&gt;[ ] False positives removed&lt;/li&gt;
&lt;li&gt;[ ] Exploitability demonstrated&lt;/li&gt;
&lt;li&gt;[ ] Impact established&lt;/li&gt;
&lt;li&gt;[ ] Attack paths analyzed&lt;/li&gt;
&lt;li&gt;[ ] Evidence collected&lt;/li&gt;
&lt;li&gt;[ ] Remediation documented&lt;/li&gt;
&lt;li&gt;[ ] Retesting performed&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Frequently Asked Questions
&lt;/h1&gt;

&lt;h2&gt;
  
  
  What is external penetration testing?
&lt;/h2&gt;

&lt;p&gt;External penetration testing simulates an attacker operating from outside an organization's network. The tester begins with publicly available information and attempts to discover and exploit internet-facing applications, services, infrastructure, APIs, and cloud resources within the authorized scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  What tools are used for external penetration testing?
&lt;/h2&gt;

&lt;p&gt;Common tools include Nmap for port and service discovery, subfinder and Amass for subdomain enumeration, httpx for HTTP probing, ffuf and Gobuster for endpoint discovery, Nuclei for automated vulnerability detection, Burp Suite for manual web testing, and tools such as TruffleHog for secret discovery.&lt;/p&gt;

&lt;p&gt;No individual tool provides complete coverage. Effective testing combines automated discovery with manual validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should be tested first in an external pentest?
&lt;/h2&gt;

&lt;p&gt;Attack-surface discovery should come before deep vulnerability testing. Start with domains, DNS, certificates, subdomains, IP ranges, cloud resources, historical URLs, and public repositories. Then move into active host discovery, port scanning, application enumeration, and vulnerability testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is an open port automatically a vulnerability?
&lt;/h2&gt;

&lt;p&gt;No. An open port indicates that a service is reachable. Whether that represents a vulnerability depends on the service, authentication, configuration, network controls, patch level, and what an attacker can accomplish after connecting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the difference between vulnerability scanning and penetration testing?
&lt;/h2&gt;

&lt;p&gt;Vulnerability scanning primarily identifies potential weaknesses using automated detection techniques. Penetration testing goes further by manually validating vulnerabilities, testing application logic, demonstrating impact, and determining whether individual weaknesses can be chained into meaningful attack paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is API testing important in external penetration testing?
&lt;/h2&gt;

&lt;p&gt;Modern applications often expose significant functionality through APIs. APIs may implement authentication and authorization differently from the web interface, and older API versions can remain accessible even after the frontend stops using them. Testing alternate API paths is therefore an important part of external attack-surface analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  How often should external penetration testing be performed?
&lt;/h2&gt;

&lt;p&gt;The required frequency depends on the organization's regulatory, contractual, and risk requirements. Periodic manual penetration testing can be supplemented by continuous external attack-surface monitoring to identify newly exposed assets and configuration drift between formal engagements.&lt;/p&gt;




&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;External penetration testing is not fundamentally a port-scanning exercise.&lt;/p&gt;

&lt;p&gt;It's an exercise in &lt;strong&gt;mapping trust boundaries from the outside&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The process starts with passive information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DNS
Certificates
ASN data
Repositories
Historical URLs
Cloud infrastructure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That becomes an asset inventory.&lt;/p&gt;

&lt;p&gt;The inventory becomes active reconnaissance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Subdomains
IPs
Ports
Services
Applications
APIs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those discoveries become testing targets.&lt;/p&gt;

&lt;p&gt;Then comes the part that requires the most judgment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is this actually vulnerable?
Can it be exploited?
What access does it provide?
Can it be chained with another weakness?
What can an attacker ultimately accomplish?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;A scanner can tell you that port 5432 is open.&lt;/p&gt;

&lt;p&gt;A penetration tester determines whether the database is authenticated, what account is accessible, what data is exposed, and whether that access creates a meaningful attack path.&lt;/p&gt;

&lt;p&gt;A scanner can discover &lt;code&gt;/api/v1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A tester determines whether the old API still works, whether authentication is enforced, whether authorization is correct, and whether it exposes functionality that &lt;code&gt;/api/v2&lt;/code&gt; properly protects.&lt;/p&gt;

&lt;p&gt;A scanner can identify a staging hostname.&lt;/p&gt;

&lt;p&gt;A tester asks why it exists, what it contains, what credentials it accepts, and what systems it can reach.&lt;/p&gt;

&lt;p&gt;That is the difference between &lt;strong&gt;finding vulnerabilities&lt;/strong&gt; and &lt;strong&gt;understanding an attack surface&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And as cloud infrastructure, APIs, CI/CD systems, and ephemeral environments continue to change, the attack surface is no longer something that can be accurately measured once a year.&lt;/p&gt;

&lt;p&gt;The methodology remains:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discover → Enumerate → Test → Validate → Exploit → Chain → Report → Retest.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The challenge is making sure you're still discovering the right things when the environment changes tomorrow.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>penetrationtesting</category>
      <category>vulnerabilities</category>
    </item>
    <item>
      <title>IDOR: The Vulnerability Class Your Scanners Will Never Find</title>
      <dc:creator>Amartya Jha</dc:creator>
      <pubDate>Tue, 08 Sep 2026 06:57:57 +0000</pubDate>
      <link>https://dev.to/codeant/idor-the-vulnerability-class-your-scanners-will-never-find-37li</link>
      <guid>https://dev.to/codeant/idor-the-vulnerability-class-your-scanners-will-never-find-37li</guid>
      <description>&lt;p&gt;In 2023, a researcher changed a single number in a URL, &lt;code&gt;/api/orders/10021&lt;/code&gt; to &lt;code&gt;/api/orders/10022&lt;/code&gt;, and got back a complete order record belonging to someone else. Name, address, items purchased, last four digits of a payment card. No exploit chain, no malware, no clever payload. Just a missing ownership check.&lt;/p&gt;

&lt;p&gt;That's IDOR: Insecure Direct Object Reference. In the OWASP API Security Top 10 it's called BOLA, Broken Object Level Authorization, and it's API1:2023. Different name, same failure: the application confirms you're logged in, but never confirms you're allowed to touch the specific object you asked for.&lt;/p&gt;

&lt;p&gt;It's arguably the most consistently present vulnerability class in modern web apps and APIs, and it causes more real SaaS data breaches than SQL injection, XSS, and RCE combined, not because it's sophisticated, but because it's invisible to most of the tooling teams rely on by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The precise definition
&lt;/h2&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%2Fv8znha1441wz9q5g71t5.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%2Fv8znha1441wz9q5g71t5.png" alt=" " width="799" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An IDOR exists when three things are all true: the app accepts a user-supplied identifier for an internal object, uses that identifier to fetch the object, and never verifies the requesting user is actually authorized to access that specific object. The vulnerability isn't in the identifier existing or being guessable. It's in that third step, the missing gap between "an ID was supplied" and "this object got returned anyway."&lt;/p&gt;

&lt;p&gt;The right security question was never "can the user change the ID." It's "after the ID changes, does the server still check whether this specific authenticated user is allowed to touch this specific object." If the answer is no, you have an IDOR/BOLA.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually looks like, across six different shapes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sequential IDs.&lt;/strong&gt; &lt;code&gt;GET /api/orders/10021&lt;/code&gt; returns User A's order legitimately. Swap it to &lt;code&gt;/api/orders/10022&lt;/code&gt;, and if that returns User B's order without an ownership check, that's the classic pattern. The sequential IDs aren't the bug, the missing authorization check on top of them is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BOLA in an API.&lt;/strong&gt; A vehicle-control API: &lt;code&gt;POST /api/vehicles/ABC123/doors/unlock&lt;/code&gt; works fine for the owner. Swap the vehicle ID to one belonging to someone else, and if the server unlocks it anyway, that's API1:2023 by definition, OWASP uses almost this exact example.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UUIDs don't fix it.&lt;/strong&gt; &lt;code&gt;GET /api/documents/f47ac10b-58cc-4372-a567-0e02b2c3d479&lt;/code&gt; looks unguessable, but User A can still get User B's UUID through a shared link, an API response, a notification, browser history, any legitimate flow. Predictability affects how easily an ID is &lt;em&gt;discovered&lt;/em&gt;. Authorization determines whether it can be &lt;em&gt;used&lt;/em&gt;. Those are separate properties, and switching ID format only fixes the first one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File references.&lt;/strong&gt; &lt;code&gt;GET /static/invoices/10021.pdf&lt;/code&gt; swapped to &lt;code&gt;10022.pdf&lt;/code&gt; is the same bug wearing a different hat, applies equally to profile images, exported reports, generated PDFs, chat transcripts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GraphQL.&lt;/strong&gt; A mutation like &lt;code&gt;deleteDocument(id: "doc-10022")&lt;/code&gt; can delete an object the caller doesn't own just as easily as a REST endpoint can leak one. The identifier's transport (URL path, query param, JSON body, GraphQL variable) is irrelevant. The server still has to authorize the object before acting on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-tenant.&lt;/strong&gt; &lt;code&gt;GET /api/organizations/acme/invoices&lt;/code&gt; swapped to &lt;code&gt;/api/organizations/other-company/invoices&lt;/code&gt; is the same failure at organizational scale, and it's the most damaging variant because a single missing check can expose an entire company's data instead of one user's.&lt;/p&gt;

&lt;h2&gt;
  
  
  The taxonomy that actually matters for triage
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal&lt;/strong&gt; — same privilege level, different user. The most common type. CVSS roughly 6.5 (read) to 8.8 (write/delete).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vertical&lt;/strong&gt; — a standard user reaches admin or elevated-privilege data by supplying a privileged resource's ID. Roughly 7.5–9.1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blind&lt;/strong&gt; — the action succeeds but nothing comes back in the response. No visible data exposure, so it's chronically underestimated, but it causes real integrity failures: deleted content, modified state, disrupted workflows, with no trace unless you check the target resource afterward. Roughly 5.0–8.8.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Second-order&lt;/strong&gt; — an identifier gets accepted in one step of a workflow, stored, and reused later without re-validating ownership. No automated tool understands multi-step context this well; the endpoint looks completely correct in isolation. Often 7.5–9.5, because it tends to bypass the most sensitive step in a workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant&lt;/strong&gt; — cross-organization access. A force multiplier: one horizontal IDOR leaks one user's data, one multi-tenant IDOR can leak an entire org's, potentially thousands of records in a single request. Almost always 8.8–9.5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mass assignment&lt;/strong&gt; — the request body includes fields like &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;owner_id&lt;/code&gt;, or &lt;code&gt;role&lt;/code&gt; that the client shouldn't be able to set, and the server processes them unfiltered.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Scoring it correctly
&lt;/h2&gt;

&lt;p&gt;Most IDOR findings get under-scored because whoever's scoring focuses on the single HTTP request instead of what it enables at scale. The good news is that almost every parameter is fixed across web/API IDORs, so scoring mostly comes down to one variable: impact.&lt;/p&gt;

&lt;p&gt;Fixed CVSS 4.0 parameters for nearly every case: Attack Vector Network, Attack Complexity Low, Attack Requirements None, Privileges Required Low, User Interaction None. Two exceptions: second-order IDORs get Attack Requirements Present, since specific workflow state has to exist first, and unauthenticated IDORs (rare, but they happen) get Privileges Required None, which pushes the score straight to critical.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;What changes&lt;/th&gt;
&lt;th&gt;CVSS 4.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read, single low-sensitivity record&lt;/td&gt;
&lt;td&gt;VC: Low&lt;/td&gt;
&lt;td&gt;~5.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read, single high-sensitivity record (PII/financial)&lt;/td&gt;
&lt;td&gt;VC: High&lt;/td&gt;
&lt;td&gt;~6.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read, all records for one user&lt;/td&gt;
&lt;td&gt;VC: High&lt;/td&gt;
&lt;td&gt;~7.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read, all records across all users&lt;/td&gt;
&lt;td&gt;VC: High + SC: High&lt;/td&gt;
&lt;td&gt;~8.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read, multi-tenant, full org exposure&lt;/td&gt;
&lt;td&gt;VC: High + SC: High&lt;/td&gt;
&lt;td&gt;~9.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write, modify another user's resource&lt;/td&gt;
&lt;td&gt;VI: High&lt;/td&gt;
&lt;td&gt;~8.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write, modify an admin-level resource&lt;/td&gt;
&lt;td&gt;VI: High + SC: High&lt;/td&gt;
&lt;td&gt;~9.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete, another user's data (blind)&lt;/td&gt;
&lt;td&gt;VI: High&lt;/td&gt;
&lt;td&gt;~7.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete, critical business records&lt;/td&gt;
&lt;td&gt;VI: High + SC: High&lt;/td&gt;
&lt;td&gt;~8.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-tenant, any of the above&lt;/td&gt;
&lt;td&gt;Always add SC: High&lt;/td&gt;
&lt;td&gt;8.8–9.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two questions decide almost the entire score: how many records are actually in scope, and can the attacker write or delete, or only read. Write and delete consistently score higher than read at equivalent scope, and multi-tenant always adds SC: High because impact extends beyond the attacker's own org.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your existing tools structurally can't catch this
&lt;/h2&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%2Fsm3jmch92qurnqndkujr.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%2Fsm3jmch92qurnqndkujr.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;DAST operates at the HTTP request level: send a request, look at the response, compare patterns. It has no concept of ownership. That's not a maturity gap in any particular tool, it's a structural limit of request-level testing without object-ownership context.&lt;/p&gt;

&lt;p&gt;Static analysis flags patterns like "identifier from user input used in a query without a visible authorization check nearby," and in practice this produces a false-positive rate that consistently exceeds 50%. Engineers burn time dismissing noise, and real IDORs buried in a complex call chain still get missed, because an authorization check exists &lt;em&gt;somewhere&lt;/em&gt; in the codebase, just not the right one at the right level for this specific resource.&lt;/p&gt;

&lt;p&gt;The actual test IDOR needs is a runtime, cross-identity question: is this authorization check actually enforced, for this resource type, from this identity, at this point in this specific workflow. That requires an authenticated identity, a resource it owns, a second identity that doesn't own it, and systematic testing of every endpoint with both, comparing results. That's a fundamentally different exercise than anything a single-request scanner does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing methodology, phase by phase
&lt;/h2&gt;

&lt;p&gt;IDOR testing needs at least two accounts with known, distinct resource ownership, and ideally two separate tenants if the app is multi-tenant. From there:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Endpoint discovery.&lt;/strong&gt; Pull every endpoint from the OpenAPI spec and from JS bundle analysis, since bundles often expose internal paths the spec omits. Flag every endpoint that accepts an object identifier, note whether it's in a path, query param, body, or header, and classify by method, since write/delete IDORs generally outscore read-only ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Horizontal testing.&lt;/strong&gt; Replay Account A's authenticated request using Account B's resource IDs, across every method, not just GET. Test batch endpoints specifically by mixing owned and unowned IDs in one request, batch endpoints frequently check authorization once for the whole collection instead of per item. Test export/download endpoints, teams treat these as secondary and skip authorization review on them constantly. Test search/filter parameters using another user's ID as the filter value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vertical testing.&lt;/strong&gt; Look for admin resource IDs leaking through error messages or metadata, then try accessing them with a standard-user token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-tenant testing.&lt;/strong&gt; Find every place &lt;code&gt;org_id&lt;/code&gt;/&lt;code&gt;tenant_id&lt;/code&gt; shows up (path, query param, body) and try substituting another org's ID in each location independently. Path-based org scoping (&lt;code&gt;/api/orgs/{org_id}/resources&lt;/code&gt;) is the most common pattern and the one most often missing validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second-order testing.&lt;/strong&gt; Map multi-step workflows, create a resource as Account A, then try to resume or complete the workflow as Account B. The common failure: ownership gets checked at creation but never re-checked at resumption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blind testing.&lt;/strong&gt; Hit every DELETE and action-POST endpoint with another user's resource ID. A 204 No Content is not a passing result by itself, the only way to confirm impact is to check the resource afterward as its actual owner and see whether it changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing it
&lt;/h2&gt;

&lt;p&gt;The fix is always server-side, object-level authorization, never identifier obscurity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# VULNERABLE
&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_or_404&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The identifier might be valid, but nothing here confirms the authenticated user owns this invoice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SECURE
&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first_or_404&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The change that matters isn't the identifier format, it's the ownership constraint applied server-side. A few patterns that generalize: derive ownership exclusively from the authenticated session, never from anything the client claims; make an ownership filter a structural requirement on every query touching user-owned data, not an optional extra; for multi-tenant apps, enforce row-level tenant isolation at the ORM or database layer so it can't be skipped by a careless handler; explicitly filter mass-assignment fields like &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;owner_id&lt;/code&gt;, and &lt;code&gt;role&lt;/code&gt; out of client-writable input; and for multi-step workflows, re-validate ownership at every single step, never assume a check from step one still holds by step three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it manually with Burp
&lt;/h2&gt;

&lt;p&gt;Browse the app authenticated, capture requests carrying anything that looks like an object reference, not just numeric IDs: UUIDs, usernames, slugs, filenames, tenant IDs, GraphQL variables. Send a request for your own object to Repeater and confirm the baseline response. Then change only the object identifier, keep the same auth token, and see what comes back.&lt;/p&gt;

&lt;p&gt;Don't trust status code alone. A vulnerable endpoint can return &lt;code&gt;200 OK&lt;/code&gt; with someone else's data, and a properly protected one might also return &lt;code&gt;200 OK&lt;/code&gt; with an error message in the body instead of a 403. Compare status, response body content, whether the object identifier in the response actually matches what was requested, response length against a known-denied baseline, and any side effects (did it modify or delete something).&lt;/p&gt;

&lt;p&gt;Confirmation requires all of: User A is authenticated, User A can access Object A, Object B belongs to User B, User A swaps the reference to B, the server returns or modifies Object B, and no additional authorization check intervened. That mismatch between "who's authenticated" and "what they were allowed to touch" is the entire vulnerability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UUID misconception, one more time
&lt;/h2&gt;

&lt;p&gt;"We switched to UUIDs so IDOR isn't a problem anymore" is one of the most persistent wrong beliefs in web security. UUIDs prevent enumeration. They do nothing to prevent unauthorized access once an identifier is obtained through some legitimate path, a shared link, a leaked response, a notification email. The fix was never about hiding the identifier. It's ownership verification at the server, full stop, and it's identical work whether the ID is &lt;code&gt;10022&lt;/code&gt; or a UUID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the blast radius here is different
&lt;/h2&gt;

&lt;p&gt;Most vulnerability classes have a bounded blast radius. XSS affects visitors to one page. A given SQL injection exposes one query's results. IDOR's blast radius scales directly with your user count and data sensitivity, because it's one missing check away from every record that check was supposed to protect. A SaaS app with 10,000 customer records has 10,000 records riding on a single ownership filter. A multi-tenant platform with 500 customer orgs has 500 organizations' worth of data one parameter swap away from any authenticated user who thinks to try it.&lt;/p&gt;

&lt;p&gt;It doesn't take exploit code or specialized tooling. It takes patience and arithmetic, and it lives entirely in application logic, which is exactly the layer most default scanning tooling doesn't reach. The only detection method that actually works is authentication-aware, ownership-tracking, multi-identity testing that asks "what if this belonged to someone else" at every endpoint, every method, every workflow step, systematically rather than opportunistically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.codeant.ai/" rel="noopener noreferrer"&gt;CodeAnt AI's penetration testing platform&lt;/a&gt; is built around exactly that cross-identity testing model, authenticating as multiple real identities simultaneously and tracking ownership across the full API surface rather than scanning requests in isolation. For the multi-tenant variant specifically, which tends to be the highest-severity flavor of this bug, see &lt;a href="https://www.codeant.ai/blog/multi-tenant-saas-penetration-testing" rel="noopener noreferrer"&gt;CodeAnt's multi-tenant SaaS penetration testing guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>appsec</category>
      <category>api</category>
    </item>
    <item>
      <title>Black Box vs White Box vs Gray Box Pentesting: Picking the Right One (Most Teams Don't)</title>
      <dc:creator>Amartya Jha</dc:creator>
      <pubDate>Mon, 07 Sep 2026 11:16:50 +0000</pubDate>
      <link>https://dev.to/codeant/black-box-vs-white-box-vs-gray-box-pentesting-picking-the-right-one-most-teams-dont-3316</link>
      <guid>https://dev.to/codeant/black-box-vs-white-box-vs-gray-box-pentesting-picking-the-right-one-most-teams-dont-3316</guid>
      <description>&lt;p&gt;"What type of penetration test do we need" is usually the second question a team asks, right after "do we need a pentest." It deserves more thought than it gets, because the three methodologies, black box, white box, and gray box, don't just differ in thoroughness. They differ in what they're structurally capable of seeing at all. A clean report from one says nothing about the other two.&lt;/p&gt;

&lt;p&gt;The difference comes down to one variable: what the tester knows and can access before the engagement starts. That starting point determines which vulnerability classes are reachable and which are, by construction, invisible to that particular test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Black box: attacker starting from nothing
&lt;/h2&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%2F4mm83mf1ai4mmy2ct1mh.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%2F4mm83mf1ai4mmy2ct1mh.png" alt=" " width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A black box tester gets a domain. No credentials, no source, no architecture docs. The question this answers is precise: what can someone on the internet, with zero inside knowledge, actually do to your data?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reconnaissance is the foundation, and it's more thorough than most teams expect.&lt;/strong&gt; Subdomain enumeration brute-forces DNS across 150+ prefix patterns, not just &lt;code&gt;www&lt;/code&gt; and &lt;code&gt;api&lt;/code&gt;, but &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, &lt;code&gt;uat&lt;/code&gt;, &lt;code&gt;internal&lt;/code&gt;, &lt;code&gt;jenkins&lt;/code&gt;, &lt;code&gt;grafana&lt;/code&gt;, &lt;code&gt;admin&lt;/code&gt;. Certificate Transparency logs get queried too, since every TLS cert ever issued for the domain is publicly logged there, which surfaces historical subdomains DNS brute-forcing misses entirely, including ones nobody remembers are still running a server. Port scanning covers all TCP ports, not just 80/443, which is how an exposed Redis instance or an unauthenticated Elasticsearch cluster gets found. It happens more than it should.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud asset discovery extends the same logic to infrastructure that isn't the app itself:&lt;/strong&gt; S3 buckets checked for public read/write, Azure Blob containers for anonymous access, GCP buckets for &lt;code&gt;allUsers&lt;/code&gt; permissions, CI/CD dashboards (Jenkins, CircleCI, GitHub Actions) checked for missing auth, monitoring endpoints (Grafana, Kibana, Datadog) checked the same way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JS bundle analysis is the step most traditional black box engagements skip, and it's one of the highest-value ones.&lt;/strong&gt; A modern SPA ships 5–15MB of minified JavaScript to every visitor's browser, and that bundle gets statically analyzed for hardcoded secrets across 30+ pattern types, AWS keys, Stripe live keys, GitHub tokens, JWT secrets, Twilio and SendGrid credentials, each one verified for validity before it's reported. Comparing staging and production bundles also surfaces endpoints that were pulled from prod but are still live on a non-production URL with weaker controls, which is a surprisingly common way to find a forgotten API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every discovered endpoint gets hit unauthenticated first&lt;/strong&gt;, and the response gets classified: &lt;code&gt;200 OK&lt;/code&gt; with real data means no auth enforced, full stop. &lt;code&gt;500&lt;/code&gt; can mean the request got processed before the auth check ever ran. &lt;code&gt;403&lt;/code&gt; gets checked for a bypass rather than taken at face value. CORS gets tested against 7+ attacker-controlled origins, since misconfigured CORS shows up in production constantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Findings get chained, not reported in isolation.&lt;/strong&gt; A tenant ID leaking from a profile endpoint plus an IDOR on the records endpoint equals full cross-tenant access. A hardcoded internal hostname in the JS bundle plus an unauthenticated endpoint on that internal API equals unauthenticated access to something that was never meant to be reachable at all. The combination is consistently more dangerous than either finding alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What black box structurally cannot see:&lt;/strong&gt; auth bypasses buried in middleware config that still return normal-looking responses, business logic flaws behind a login wall, secrets sitting in Git history, anything on an internal service never exposed to the internet, dependency vulnerabilities that need code access to assess reachability. This isn't a weakness in the methodology, it's the direct consequence of the threat model. A black box test simulates someone with nothing. It can only ever see what nothing gets you.&lt;/p&gt;

&lt;h2&gt;
  
  
  White box: reading the implementation directly
&lt;/h2&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%2Fblmj6o53a78faybzzkgd.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%2Fblmj6o53a78faybzzkgd.png" alt=" " width="800" height="577"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;White box gives the tester the code. Configs, infrastructure definitions, dependency manifests, architecture docs, version history. The question shifts from "what can an attacker discover" to "what's actually wrong in the implementation, whether or not it's visible from outside."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security configuration gets read directly, which catches things scanning never will.&lt;/strong&gt; A Spring Security filter chain excluded for an entire &lt;code&gt;/api/v2/&lt;/code&gt; namespace looks completely normal from outside, the endpoint just responds. An external scanner has no way to know the auth layer got skipped entirely for that namespace. Same story with Express middleware ordering: an admin endpoint can return &lt;code&gt;200 OK&lt;/code&gt; with real data to an unauthenticated request because the auth middleware got registered after the route handler, and nothing about the HTTP response gives that away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets scanning covers current HEAD and history separately.&lt;/strong&gt; A credential committed and later deleted from the working tree is still sitting in version control, recoverable by anyone with clone access, and that's a distinct check from scanning what's currently checked in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dataflow tracing is where white box earns its reputation for precision.&lt;/strong&gt; Instead of "SQL injection detected," a proper trace produces something like: &lt;code&gt;app/views/products.py&lt;/code&gt;, line 14, &lt;code&gt;search_products()&lt;/code&gt;, the &lt;code&gt;category&lt;/code&gt; parameter from &lt;code&gt;request.GET&lt;/code&gt; reaches a raw query via string formatting, payload &lt;code&gt;' OR '1'='1' --&lt;/code&gt;, effect: returns all products regardless of category or featured status, root cause: &lt;code&gt;Product.objects.raw()&lt;/code&gt; with f-string interpolation instead of a parameterized query. That specificity is the difference between an engineer fixing the actual line and an engineer guessing at what "SQL injection" means for their codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependency analysis goes past CVE matching into reachability.&lt;/strong&gt; A vulnerable package that's imported but never actually called from a code path the app executes is a very different risk than the same CVE sitting in a function that runs on every file upload. Reachability analysis is what separates the two, and it's the single biggest lever for cutting dependency-scan false positives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gray box: what a legitimate account can get away with
&lt;/h2&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%2Feeau5mzv4thk3huwbvof.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%2Feeau5mzv4thk3huwbvof.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gray box starts with authenticated access, one or more real test accounts, sometimes limited docs or API specs on top. The question: what can someone do once they already have valid credentials?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Access control and privilege escalation testing hits every admin-level endpoint with non-admin credentials&lt;/strong&gt;, and JWT claims get manipulated directly to check whether role or tenant claims are actually re-validated server-side or just trusted from the token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IDOR testing here is systematic, not exploratory.&lt;/strong&gt; Every endpoint accepting an object identifier, sequential integer, UUID, slug, username, filename, tenant ID, or an ID buried in a JSON or GraphQL body, gets evaluated for object-level authorization. The core test: can a user authorized for Object A simply substitute Object B's identifier and read, modify, or delete it. In multi-tenant apps this same check has to run at the org boundary, not just the user boundary, since a missing tenant check leaks an entire company's data instead of one account's. (&lt;a href="https://www.codeant.ai/blogs/idor-vulnerabilities" rel="noopener noreferrer"&gt;We've written the technical breakdown of this vulnerability class in full&lt;/a&gt;, including every variant and how to test for it, if you want to go deeper than this section.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Business logic testing is where gray box finds things nothing else reaches&lt;/strong&gt;, because none of these produce an anomalous HTTP response or match a known CVE signature. They require understanding what the app is supposed to enforce, then checking whether it actually does, at every entry point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can the order total be modified in the request before payment confirms?&lt;/li&gt;
&lt;li&gt;Can a single-use discount code be replayed by resending the validation call?&lt;/li&gt;
&lt;li&gt;Can checkout step 5 be hit directly without completing steps 1 through 4?&lt;/li&gt;
&lt;li&gt;Can a free-tier account call a premium endpoint directly via the API?&lt;/li&gt;
&lt;li&gt;Can rate limits be evaded by rotating user IDs or spoofed IP headers?&lt;/li&gt;
&lt;li&gt;Can negative quantities reduce a total in an e-commerce flow?&lt;/li&gt;
&lt;li&gt;Does a race condition in inventory or balance checks survive two simultaneous requests?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this shows up in a scanner's output. All of it ships to production regularly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison that actually matters
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Black box&lt;/th&gt;
&lt;th&gt;White box&lt;/th&gt;
&lt;th&gt;Gray box&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simulates&lt;/td&gt;
&lt;td&gt;External attacker, no prior knowledge&lt;/td&gt;
&lt;td&gt;Insider/attacker with implementation access&lt;/td&gt;
&lt;td&gt;Legitimate user or compromised account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starting point&lt;/td&gt;
&lt;td&gt;Domain + public info&lt;/td&gt;
&lt;td&gt;Source, config, architecture, dependencies&lt;/td&gt;
&lt;td&gt;Test credentials + selected context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strong at&lt;/td&gt;
&lt;td&gt;Exposed services, unauth endpoints, cloud misconfig&lt;/td&gt;
&lt;td&gt;Code-level auth flaws, data flow issues, secrets, dependency risk&lt;/td&gt;
&lt;td&gt;IDOR, privilege escalation, broken access control, business logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structurally misses&lt;/td&gt;
&lt;td&gt;Anything needing auth or internal visibility&lt;/td&gt;
&lt;td&gt;Runtime behavior specific to production conditions&lt;/td&gt;
&lt;td&gt;Unauthenticated external exposure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answers&lt;/td&gt;
&lt;td&gt;"What can an external attacker reach?"&lt;/td&gt;
&lt;td&gt;"What's actually wrong in the implementation?"&lt;/td&gt;
&lt;td&gt;"What can an authenticated user get away with?"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these are difficulty tiers stacked on top of each other. They're three different threat models with three different structural blind spots, and the important asymmetry is this: a clean result from any one of them tells you nothing about the other two. A spotless black box report doesn't mean the code is clean. A spotless white box audit doesn't mean nothing's exposed externally. A spotless gray box assessment says nothing about whether the unauthenticated surface holds up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking one (or more)
&lt;/h2&gt;

&lt;p&gt;If you've never had a real assessment, run all three together. Partial coverage produces the worst outcome in security, which is false confidence from a report that never looked where the actual risk was sitting.&lt;/p&gt;

&lt;p&gt;If you're pre-launch with a product handling customer data, prioritize gray box plus white box. Business logic flaws and code-level auth bugs are exactly what ships in a first release, and the external surface can be addressed continuously once the app is actually live and has real exposure to test against.&lt;/p&gt;

&lt;p&gt;If you've already run black box tests before (most first pentests are black box by default) and never gone further, white box is very likely your highest-value next investment. Most prior engagements never touched the code, and that's where the deepest, highest-severity findings tend to live.&lt;/p&gt;

&lt;p&gt;If your specific concern is a compromised account crossing into another customer's data, gray box is the direct answer, since it's the only methodology built to test tenant and object-level boundaries under real authenticated conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why chaining across all three beats running them separately
&lt;/h2&gt;

&lt;p&gt;The real value isn't additive coverage, it's that findings from one perspective give the other two something to test against. Black box finds an exposed API. White box traces that same endpoint into its authorization logic and shows exactly how (or whether) access gets enforced. Gray box then hits that endpoint with real credentials to check whether an authenticated user can swap an object ID and reach someone else's data.&lt;/p&gt;

&lt;p&gt;A vulnerability that spans layers like this is easy to miss with any single methodology and hard to miss once the three are cross-referenced against each other. An exposed endpoint only becomes dangerous paired with a missing authorization check behind it. A code-level weakness only becomes exploitable once you know the corresponding endpoint is actually reachable from outside. Treat black box, white box, and gray box as three independent security programs and you'll keep missing exactly this kind of finding, the one that only exists at the seam between them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.codeant.ai/" rel="noopener noreferrer"&gt;CodeAnt AI's penetration testing platform&lt;/a&gt; runs black box, white box, and gray box as a single engagement with attack-chain validation across all three, rather than three disconnected reports that never talk to each other.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>pentesting</category>
      <category>appsec</category>
    </item>
    <item>
      <title>Red Team Authorization: Solving the Paradox of Testing People Who Can't Know They're Being Tested</title>
      <dc:creator>Amartya Jha</dc:creator>
      <pubDate>Mon, 07 Sep 2026 07:48:24 +0000</pubDate>
      <link>https://dev.to/codeant/red-team-authorization-solving-the-paradox-of-testing-people-who-cant-know-theyre-being-tested-2omb</link>
      <guid>https://dev.to/codeant/red-team-authorization-solving-the-paradox-of-testing-people-who-cant-know-theyre-being-tested-2omb</guid>
      <description>&lt;p&gt;A red team engagement exists to answer one question: can your security team detect and respond to a real attack. For that answer to mean anything, the security team being tested, the blue team, cannot know the test is happening.&lt;/p&gt;

&lt;p&gt;That single constraint breaks the normal authorization model. Standard authorization flows through the people who own the systems being tested, and for a pentest, that includes security and IT. For a red team, those are exactly the people who have to stay in the dark. Get this wrong in one direction and the blue team finds out, which ruins the test. Get it wrong in the other direction and nobody with real authority actually approved the engagement, which puts the testers in genuine legal exposure if something goes sideways mid-operation. Both failure modes defeat the entire point of running a red team in the first place.&lt;/p&gt;

&lt;p&gt;The fix is compartmentalization: authorization flows from a small group of executives who are explicitly, provably not part of the blue team being evaluated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this isn't just a stricter pentest authorization
&lt;/h2&gt;

&lt;p&gt;A red team authorization letter is a different document with different legal and operational requirements, not a pentest letter with a tighter NDA bolted on. A red team may deliberately interact with people, facilities, and defensive systems that have no idea a test is running.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Standard pentest&lt;/th&gt;
&lt;th&gt;Red team engagement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Objective&lt;/td&gt;
&lt;td&gt;Identify and validate vulnerabilities&lt;/td&gt;
&lt;td&gt;Test detection, response, and containment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who knows&lt;/td&gt;
&lt;td&gt;Security, IT, system owners&lt;/td&gt;
&lt;td&gt;Restricted executive/legal group; blue team often excluded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signing authority&lt;/td&gt;
&lt;td&gt;CISO, CTO, or delegated security exec&lt;/td&gt;
&lt;td&gt;Exec sponsor with authority over the target, independent of the blue team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;Apps, APIs, infra, cloud assets&lt;/td&gt;
&lt;td&gt;Technical systems plus facilities, personnel, identities, defensive controls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Social engineering&lt;/td&gt;
&lt;td&gt;Usually excluded or separately authorized&lt;/td&gt;
&lt;td&gt;May be explicitly authorized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Physical access&lt;/td&gt;
&lt;td&gt;Usually excluded or separately authorized&lt;/td&gt;
&lt;td&gt;May include offices, badge systems, tailgating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blue-team awareness&lt;/td&gt;
&lt;td&gt;Usually expected&lt;/td&gt;
&lt;td&gt;Often intentionally restricted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emergency contact&lt;/td&gt;
&lt;td&gt;Testing firm + client security contact&lt;/td&gt;
&lt;td&gt;24/7 executive sponsor who can confirm the op immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Law enforcement&lt;/td&gt;
&lt;td&gt;Addressed in emergency procedures&lt;/td&gt;
&lt;td&gt;Must explicitly define how authorization gets verified on the spot&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Who's actually allowed to sign
&lt;/h2&gt;

&lt;p&gt;The signing rule is simple to state and easy to get wrong in practice: the signer cannot be part of the group whose detection capability is being evaluated.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the CISO manages the SOC being tested, the CISO cannot sign.&lt;/li&gt;
&lt;li&gt;If the entire security function is in scope, it goes to CEO or board level.&lt;/li&gt;
&lt;li&gt;If security manages but doesn't operationally run the specific test target, the CISO may sign, provided they're genuinely separate from the blue team in question.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't "the CEO always signs." It's that the signer needs actual authority over the assets in scope and needs to sit outside the operational group being evaluated. Most red team authorizations end up at CEO or board level for a simple reason: the engagement is testing the security function itself, and the person who runs that function can't objectively authorize their own team's evaluation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud providers have their own rules, and they change
&lt;/h2&gt;

&lt;p&gt;Red team authorization from your organization doesn't extend to permission from your cloud provider. AWS, Azure, and GCP each publish their own acceptable-use and security-testing policies, and they get updated. Verify the current version immediately before the engagement rather than trusting whatever was true last year.&lt;/p&gt;

&lt;p&gt;For every cloud provider in scope, document: the provider and account/subscription/project identifier, specific regions and resources, the approved testing window, approved and explicitly prohibited techniques, any notification or approval requirements the provider imposes, emergency contacts, executive authorization, and stop conditions. Treat this as its own verification step, separate from the signed authorization letter, not something you assume is automatically covered by it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sealed envelope model
&lt;/h2&gt;

&lt;p&gt;When getting an executive signature at engagement start is genuinely difficult for legal, timing, or organizational reasons, a sealed envelope approach works: the fully signed authorization exists, but it's only opened if a legal issue comes up, law enforcement gets involved, or the organization faces an inquiry that requires documented proof. This lets the engagement run under strict compartmentalization while real, verifiable authorization still exists if it's ever needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The get-out-of-jail card
&lt;/h2&gt;

&lt;p&gt;Anyone doing physical access or social engineering work in the field should be carrying a physical card, not a digital one, that lets them verify the engagement on the spot if security or police stop them.&lt;/p&gt;

&lt;p&gt;Requirements that actually matter here: a 24/7-reachable number, not an office line that goes to voicemail at 11pm; an authentication code the contact can verify against; the named contact is the executive sponsor, never someone on the security team; every field operator carries one; and it's a printed copy, because a phone can be confiscated, dead, or locked during exactly the moment you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the rules of engagement have to nail down
&lt;/h2&gt;

&lt;p&gt;The authorization letter grants permission. The rules of engagement define the edges of that permission, and for a red team both documents matter equally: objectives (detection, response, containment, physical security, identity, or full lifecycle), in-scope and out-of-scope targets down to specific systems and facilities, permitted and prohibited techniques, the exact testing window, how discovered sensitive data gets handled, whether persistence is allowed and when it must be removed, the detection protocol once the blue team notices, stop conditions, emergency contacts, the law-enforcement verification procedure, third-party boundaries, evidence preservation, and cleanup requirements at close.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when the blue team actually catches you
&lt;/h2&gt;

&lt;p&gt;This is the part standard pentest authorization letters never have to address, and it's where most of the real risk lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blue team detects but doesn't escalate to law enforcement.&lt;/strong&gt; Define in advance whether the red team keeps operating as if undetected, or stops. Most engagements specify continuing unless the executive sponsor says otherwise. This should never be improvised live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blue team escalates to the CISO.&lt;/strong&gt; If the CISO is in the "doesn't know" group, they might treat this as a real incident and call the police. The authorization needs a defined procedure for the executive sponsor to step in before that happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blue team calls law enforcement.&lt;/strong&gt; This is the scenario that produced the well-known Coalfire arrests. Officers show up, testers get detained. The get-out-of-jail card is the first line of resolution. The 24/7 executive sponsor is the second, and the letter needs to state plainly that they're reachable to confirm the engagement directly to law enforcement, not to a lawyer, not by email, directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blue team successfully contains the attack.&lt;/strong&gt; Decide ahead of time whether the engagement resets, ends, or triggers an immediate debrief, so nobody's making that call under pressure in the moment.&lt;/p&gt;

&lt;p&gt;At minimum, pre-define stop conditions for: production availability materially affected, customer data placed at risk, an unintended third party affected, law enforcement involvement, the blue team activating emergency response, the red team hitting a prohibited system, or the executive sponsor simply ordering a stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a red team letter needs that a standard one doesn't
&lt;/h2&gt;

&lt;p&gt;A compartmentalization section naming exactly who knows the engagement is happening and who must not be told, by name, not by role. A written confirmation from the executive sponsor that they're reachable for the full engagement window, treated as a primary operational requirement rather than a footnote. A defined blue-team response protocol so nobody improvises under pressure. A documented decision on whether local law enforcement was pre-notified, which is genuinely the single most effective way to avoid an arrest scenario. Precise physical scope, "the company's offices" is not scope, list addresses, floors, access types, and whether tailgating or badge cloning are in bounds. Verified cloud-provider authorization per account. An explicit reference to the signed rules of engagement so there's no ambiguity about what's actually authorized. And a named person with clear stop/terminate authority, plus the conditions under which the team must halt without waiting for sign-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permission under secrecy is a different problem than permission
&lt;/h2&gt;

&lt;p&gt;A standard pentest is about getting permission. A red team is about getting permission while deliberately keeping most of the organization unaware it exists, and that constraint changes the shape of every document involved. Authorization can't be broad, can't be assumed, and can't reuse the pentest template with a confidentiality clause added. The moment the wrong people find out, the test is worthless. The moment the right people haven't formally signed off, the engagement is a real legal liability for everyone in the field.&lt;/p&gt;

&lt;p&gt;Limited awareness, explicit authority, and a clear escalation path, defined before testing starts, is the whole model. Get it right and the engagement runs exactly as designed: realistic, controlled, defensible. Get it wrong and you either lose the test or put your own team at risk. There's no middle version of this.&lt;/p&gt;

&lt;p&gt;For the baseline structure this builds on, see &lt;a href="https://www.codeant.ai/blog/penetration-test-authorization-letter" rel="noopener noreferrer"&gt;CodeAnt AI's guide to penetration test authorization letters&lt;/a&gt;. For how these engagements get scoped and reported end to end, see &lt;a href="https://www.codeant.ai/" rel="noopener noreferrer"&gt;CodeAnt AI's penetration testing platform&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>informationsecurity</category>
      <category>redteam</category>
      <category>pentesting</category>
    </item>
    <item>
      <title>SOC 2 Penetration Testing: What Auditors Actually Check For (And Where Most Programs Fail)</title>
      <dc:creator>Amartya Jha</dc:creator>
      <pubDate>Mon, 07 Sep 2026 07:21:44 +0000</pubDate>
      <link>https://dev.to/codeant/soc-2-penetration-testing-what-auditors-actually-check-for-and-where-most-programs-fail-3neh</link>
      <guid>https://dev.to/codeant/soc-2-penetration-testing-what-auditors-actually-check-for-and-where-most-programs-fail-3neh</guid>
      <description>&lt;p&gt;Day three of a SOC 2 Type II audit. The auditor pulls up the penetration testing section of the audit program and asks: "Can you show me evidence that exploitable vulnerabilities identified during your penetration test were corrected, and that the corrections were tested?"&lt;/p&gt;

&lt;p&gt;Not "did you do a pentest." Not "can I see the report." The question is specifically about correction and verification.&lt;/p&gt;

&lt;p&gt;The engineering lead can produce the pentest report without much trouble. A retest report that confirms each finding was actually patched and verified in production is harder to find. A documented timeline from finding to fix to verification, harder still. Evidence that the retest ran against the same environment, same methodology, as the original test, almost nobody has that on hand.&lt;/p&gt;

&lt;p&gt;The control gets marked as an exception. The report ships with a qualified opinion on CC7.1. The enterprise sales cycle gets a little longer.&lt;/p&gt;

&lt;p&gt;This plays out constantly, not because companies skip security testing, most don't, but because they run testing without understanding what SOC 2 specifically needs that testing to produce as evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  SOC 2 is an opinion, not a certificate
&lt;/h2&gt;

&lt;p&gt;SOC 2 (Service Organization Control 2) is an AICPA auditing standard. It evaluates whether your controls meet the Trust Services Criteria relevant to your service. It is not a certification body handing out badges, it's an attestation: an independent CPA firm's formal opinion that your controls meet the criteria. That opinion is the report you hand to customers.&lt;/p&gt;

&lt;p&gt;Five TSC categories exist. Security (CC) is mandatory. The other four are optional add-ons depending on what you handle:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Required?&lt;/th&gt;
&lt;th&gt;Pentest relevance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Security (CC)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Direct — CC6, CC7, CC8, CC9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability (A)&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Infra resilience, DDoS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processing Integrity (PI)&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Business logic, data validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confidentiality (C)&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Data access, encryption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy (P)&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;PII handling, access verification&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's the part that trips people up: SOC 2 barely uses the phrase "penetration testing" anywhere in its text. Companies search for an explicit line item, don't find one, and conclude testing is optional. It isn't. The requirement is implicit in CC6.6 (protection against external threats) and CC7.1 (detection of security events), and AICPA's supplemental guidance explicitly names penetration testing as an appropriate way to satisfy them. Every Big Four firm and most regional CPA firms with a SOC 2 practice treat annual pentesting as table stakes for the Security category.&lt;/p&gt;

&lt;p&gt;The real question was never whether you need a pentest. It's whether the one you ran generates the specific evidence an auditor is trained to look for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type I vs Type II: different questions entirely
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Type I&lt;/th&gt;
&lt;th&gt;Type II&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Core question&lt;/td&gt;
&lt;td&gt;Are controls suitably designed?&lt;/td&gt;
&lt;td&gt;Did controls operate effectively over time?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;For pentest&lt;/td&gt;
&lt;td&gt;Do you have a program, and is it reasonably designed?&lt;/td&gt;
&lt;td&gt;Did the program actually run and produce results people acted on?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Type I&lt;/strong&gt; checks that a program exists and looks reasonable. You need: a written, dated, approved pentest policy; one completed test report inside the observation window; evidence the tester was qualified and independent; a finding list with severities; a remediation plan (doesn't need to be finished, just documented). Type I auditors will not ask for a retest report, proof that remediation worked, or evidence testing covered everything critical. They're checking existence and design, not execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type II&lt;/strong&gt; checks execution across the whole period, and the evidence bar jumps considerably:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pentest report(s) covering the observation period&lt;/li&gt;
&lt;li&gt;Evidence critical/high findings were actually remediated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A retest report confirming remediation worked&lt;/strong&gt; — the single most commonly missing item&lt;/li&gt;
&lt;li&gt;Timeline evidence: find date, fix date, retest date&lt;/li&gt;
&lt;li&gt;Remediation SLA documentation and proof you hit it&lt;/li&gt;
&lt;li&gt;Evidence the test covered systems that touch customer data&lt;/li&gt;
&lt;li&gt;Risk acceptance documentation for anything left unremediated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common failure patterns at Type II, roughly in order of frequency: a pentest report exists but no retest report does; the retest didn't cover every original finding; remediation happened in staging, not production (production is what's audited, staging evidence doesn't count); remediation took longer than the stated SLA, which is worse than having no SLA; risk acceptances existed only verbally; the test ran against an older version than what's in production; the gap between the last test and the audit exceeds twelve months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timing rules nobody reads until it's too late
&lt;/h2&gt;

&lt;p&gt;SOC 2 Type II has a minimum six-month observation period per AICPA guidance, though in practice enterprise buyers expect twelve. Your testing needs to produce evidence across that whole window, not one point inside it.&lt;/p&gt;

&lt;p&gt;Three structural options, in order of evidence strength:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Cadence&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Annual (1 test + 1 retest)&lt;/td&gt;
&lt;td&gt;System as of test date&lt;/td&gt;
&lt;td&gt;Stable, low-change systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Semi-annual (2 tests + 2 retests)&lt;/td&gt;
&lt;td&gt;Two snapshots across the period&lt;/td&gt;
&lt;td&gt;Most SaaS teams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Continuous (monthly)&lt;/td&gt;
&lt;td&gt;Full period&lt;/td&gt;
&lt;td&gt;High-velocity development&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rules that catch teams off guard:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The test must occur &lt;em&gt;inside&lt;/em&gt; the observation period. A test run two months before your period started evidences the prior period, not this one.&lt;/li&gt;
&lt;li&gt;For a twelve-month period, at least one test needs to land inside those twelve months.&lt;/li&gt;
&lt;li&gt;The retest must also happen inside the period, after remediation.&lt;/li&gt;
&lt;li&gt;The gap between your last test and period-end shouldn't exceed roughly six months. Beyond that, expect the auditor to ask about it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The single most common mistake: running the pentest in Q1 for a Q1–Q4 audit period and never scheduling a follow-up. The auditor eventually asks what covered Q3 and Q4. The honest answer, nothing, doesn't cause an automatic qualified opinion by itself, but it's a gap you'll have to explain, and it's rarely a satisfying explanation. Fix: schedule a second test, or at minimum a targeted retest, around the six-month mark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope: follow the data, not the architecture diagram
&lt;/h2&gt;

&lt;p&gt;The scoping principle that matters more than any other: test everything that touches, stores, processes, or transmits the data your SOC 2 covers. Testing systems that don't touch customer data while skipping the ones that do is a scoping failure auditors catch quickly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;In scope?&lt;/th&gt;
&lt;th&gt;Controls&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Public web app&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;CC6.6&lt;/td&gt;
&lt;td&gt;Primary requirement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authenticated API&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;CC6.1, CC6.3&lt;/td&gt;
&lt;td&gt;Document endpoint coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin panel&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;CC6.1, CC6.3&lt;/td&gt;
&lt;td&gt;High-value target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer data store&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;CC6.1, C1.1&lt;/td&gt;
&lt;td&gt;Often tested via the app layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud IAM&lt;/td&gt;
&lt;td&gt;Yes (if cloud-hosted)&lt;/td&gt;
&lt;td&gt;CC6.6, CC8.1&lt;/td&gt;
&lt;td&gt;IAM privilege audit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD pipeline&lt;/td&gt;
&lt;td&gt;Recommended&lt;/td&gt;
&lt;td&gt;CC8.1&lt;/td&gt;
&lt;td&gt;High supply-chain risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile apps&lt;/td&gt;
&lt;td&gt;Recommended&lt;/td&gt;
&lt;td&gt;CC6.6&lt;/td&gt;
&lt;td&gt;If customer-facing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging/QA&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Test production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third-party SaaS tools&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;CC9.2&lt;/td&gt;
&lt;td&gt;Review their SOC 2 instead&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two scope gaps auditors flag constantly: cloud infrastructure ("AWS manages that" is not a scope rationale, IAM misconfigurations and exposed buckets are CC6.1/CC6.6 findings and belong explicitly in scope) and APIs (most breaches happen at the API layer, but plenty of testing programs only cover what renders in a browser).&lt;/p&gt;

&lt;h2&gt;
  
  
  The retest report is the thing nobody has
&lt;/h2&gt;

&lt;p&gt;If there's one document to obsess over, it's this one. Fixing a bug isn't the same as evidencing the fix worked. The retest needs to come from the same external firm that ran the original test, re-executing the original test cases against the patched, production system, and producing a second report confirming which findings actually closed. Internal QA can supplement this, it can't replace it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What auditors actually ask, verbatim-adjacent
&lt;/h2&gt;

&lt;p&gt;These aren't hypothetical. They're the standard interview pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Do you have a penetration testing policy?" — wants a written, approved, dated document, not "yes, we do pentests"&lt;/li&gt;
&lt;li&gt;"When was your last test?" — wants a date inside the observation period&lt;/li&gt;
&lt;li&gt;"Who conducted it?" — wants a named external firm, not "our developer ran some scans"&lt;/li&gt;
&lt;li&gt;"How do you know remediations worked?" — wants a retest report, not self-attestation&lt;/li&gt;
&lt;li&gt;"Show me a critical finding remediated within your SLA" — wants discovery, fix, and retest dates all inside the policy timeline&lt;/li&gt;
&lt;li&gt;"What about findings you didn't fix?" — wants a signed risk acceptance with a named owner and compensating controls&lt;/li&gt;
&lt;li&gt;"Was the test conducted against production?" — many teams test staging to avoid disruption; auditors know this trick and ask directly&lt;/li&gt;
&lt;li&gt;"Did the test cover cloud infrastructure?" — app testing and cloud config review are not the same exercise&lt;/li&gt;
&lt;li&gt;"What version was running when the test occurred?" — a test of v1.2 doesn't evidence v2.8; put the version number in the authorization letter&lt;/li&gt;
&lt;li&gt;"Are you getting better year over year?" — wants CVSS severity trend data across periods; a flat or worsening trend is a flag in renewal audits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern behind every good answer is the same: name the document, cite the date, name the firm, show the timeline. If you can't do that in one sentence, that's the gap.&lt;/p&gt;

&lt;p&gt;Nearly every exception traces back to one of three root causes: no retest report, timing (test ran outside the observation window or was never followed up), or scope (staging instead of production, no cloud layer, browser-only coverage while the API sat untested).&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the program by stage
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;First Type I audit.&lt;/strong&gt; Start four months out, not four weeks. Month one: write and approve the policy, pick a qualified external vendor, scope every system touching customer data, run the test. Month two: triage findings with owners, remediate all criticals before the audit (highs strongly recommended), document risk acceptances for anything left, book the retest. Month three: get the retest report, assemble the evidence package, get management sign-off on findings (minuted), hand it to the auditor. Budget for a sub-50-endpoint SaaS: roughly $8K–$20K for the test, $2K–$6K for the retest, $1K–$3K for policy support if starting cold. The most common failure at this stage is simply starting too late; four weeks isn't enough runway for test, remediation, and retest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Growth stage, Type II with active shipping.&lt;/strong&gt; A single annual test doesn't cover code shipped in month nine of a twelve-month period. Reasonable structure: one comprehensive annual test as the baseline, quarterly targeted testing of high-change areas, a security review on any auth/authz change before release, and continuous DAST/SAST/SCA scanning filling the gaps. The most common gap here is months seven through twelve with zero testing evidence, plus new features (a new payment flow, new auth method) shipped after the annual test with no review at all, which is a direct CC8.1 problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise, Type II with MSA obligations.&lt;/strong&gt; Baseline SOC 2 requirements plus whatever your customer contracts separately demand, and these are two different obligation sets, don't conflate them. Enhanced testing typically means external perimeter, internal network, API, and cloud reviewed together rather than separately, security testing embedded in the SDLC instead of bolted on annually, and a red team engagement every two to three years. Some enterprise customers contractually require quarterly testing, full report access instead of an executive summary, specific tester certifications (CREST, for instance), or a 24–48 hour critical-finding notification window. None of that is SOC 2, it's contract, and it needs its own evidence trail separate from the standard package.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a provider without guessing
&lt;/h2&gt;

&lt;p&gt;SOC 2 doesn't name specific certifications. It requires a "qualified" party, and your auditor applies judgment to decide whether that bar was met, so you need a defensible case for your choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-negotiable floor:&lt;/strong&gt; external to your org (internal or affiliated testers don't satisfy independence, full stop), a formal written report (a spreadsheet of findings is not audit evidence), and a formal retest included in scope, not "we'll review your remediation notes."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signals worth paying for:&lt;/strong&gt; CREST organizational certification, OSCP or CISSP on the individual testers, prior SOC 2-specific engagement experience (they should already know what "per-finding verification" means to an auditor, you shouldn't be the engagement where they learn it).&lt;/p&gt;

&lt;p&gt;Questions worth asking before signing: can you show a sanitized sample report with unique finding IDs, CVSS scores, and proof-of-concept evidence (not scanner output with a paragraph slapped on)? Is retest included and does it produce its own separate report? Will you confirm in writing you're testing production? What's your process when you find a critical mid-engagement (there should be an out-of-band escalation, not "wait for the final report")? Will you provide a CVSS delta against last year's findings for renewal purposes?&lt;/p&gt;

&lt;p&gt;Red flags that end the conversation: automated scanner output passed off as a formal report, no retest service, no unique finding IDs, an inability to map findings to specific TSC control numbers, a default to staging, no professional liability insurance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The twelve mistakes that actually cause exceptions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Process failures, before the test runs:&lt;/strong&gt; testing staging instead of production; skipping the retest or letting an internal team do it; leaving critical findings open at audit time (start 90 days out, not 30); running the test before the observation period even opens; switching testing firms every year, which kills your ability to show a CVSS trend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation failures, after the test:&lt;/strong&gt; finding triage that happened but was never logged in a ticketing system; management review of the security posture that happened in someone's head instead of in minutes (CC5.3 needs the paper trail); risk acceptances that were a verbal "yeah, we'll live with that" instead of a signed document with a named owner; no sign-off that the scope actually covered customer-data systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Policy failures, the ones that look like organizational dysfunction:&lt;/strong&gt; an SLA policy that says 48 hours for criticals next to a remediation log showing 30 days, which is worse evidence than having no SLA at all; a scope document that excludes cloud infrastructure with "AWS handles that" as the justification.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a well-run engagement actually produces
&lt;/h2&gt;

&lt;p&gt;A pentest built for SOC 2 evidence, not just technical findings, structures itself around the control mapping from the start: reconnaissance covering every external asset for CC6.6; source-level tracing of auth and authz flows for CC6.1/CC6.3; JS bundle analysis for leaked secrets and exposed endpoints under CC6.7; active exploitation with every finding confirmed working before it's reported, chained findings reported as chains (three mediums that combine into one critical shouldn't ship as three tickets your team deprioritizes); a report where every finding carries proof-of-exploit, root cause down to file and line, CVSS 4.0 with metric justification, and a mapping to the specific control ID, not a general "this relates to SOC 2." Then unlimited retesting at no extra cost, with the retest confirming remediation in production specifically, and a named researcher sign-off, which is what satisfies the "qualified party" language in your own policy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.codeant.ai/" rel="noopener noreferrer"&gt;CodeAnt AI's penetration testing platform&lt;/a&gt; is built around exactly this evidence structure, which is also what makes running the semi-annual or continuous testing cadence operationally realistic instead of a once-a-year scramble.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual takeaway
&lt;/h2&gt;

&lt;p&gt;Nobody fails a SOC 2 pentest control because their engineers can't find vulnerabilities. They fail it because the retest report doesn't exist, the test ran outside the observation window, or the scope quietly excluded the systems that actually touch customer data. All three are process problems, not technical ones, and all three are fixable months before an auditor ever sits down across the table.&lt;/p&gt;

&lt;p&gt;For a deeper breakdown of testing cadence tradeoffs referenced above, see &lt;a href="https://www.codeant.ai/blog/continuous-vs-annual-penetration-testing" rel="noopener noreferrer"&gt;continuous vs. annual penetration testing&lt;/a&gt;. For what a retest engagement should actually contain, see &lt;a href="https://www.codeant.ai/blog/pentest-retest" rel="noopener noreferrer"&gt;what happens during a pentest retest&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>compliance</category>
      <category>cybersecurity</category>
      <category>soc2</category>
    </item>
    <item>
      <title>The Deployment Velocity Gap: Why Annual Pentesting Can't Keep Up With Modern SaaS</title>
      <dc:creator>Amartya Jha</dc:creator>
      <pubDate>Mon, 07 Sep 2026 06:03:28 +0000</pubDate>
      <link>https://dev.to/codeant/the-deployment-velocity-gap-why-annual-pentesting-cant-keep-up-with-modern-saas-d51</link>
      <guid>https://dev.to/codeant/the-deployment-velocity-gap-why-annual-pentesting-cant-keep-up-with-modern-saas-d51</guid>
      <description>&lt;p&gt;Most SaaS teams still pentest once a year. Almost none of them ship code once a year.&lt;/p&gt;

&lt;p&gt;That mismatch is the actual security problem worth talking about, more than any single vulnerability class. Security gets validated at one cadence. Risk gets introduced at another. The distance between those two cadences has a name: the deployment velocity gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the gap actually is
&lt;/h2&gt;

&lt;p&gt;The deployment velocity gap is the time between a security-relevant change hitting production and that change actually being evaluated by a penetration test.&lt;/p&gt;

&lt;p&gt;In an annual model, that gap can run for months. A system might be genuinely secure the day the assessment wraps, but every deploy after that day creates new, untested surface area: new endpoints, an updated auth flow, a new third-party integration, a reconfigured piece of infrastructure. None of it was in scope for the test that already happened. All of it is in scope for the breach that hasn't happened yet.&lt;/p&gt;

&lt;p&gt;This isn't a knock on pentesting as a discipline. It's a scoping problem. A penetration test evaluates a moment in time, and it evaluates a defined scope, both of which shrink in relative value as deployment frequency rises.&lt;/p&gt;

&lt;p&gt;You can put a number on your own exposure with something this simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deployment_velocity_gap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_interval_days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weekly_deployments&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    test_interval_days: 365 = annual, 90 = quarterly, 30 = monthly
    weekly_deployments: how many times code ships per week
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;average_gap_days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;test_interval_days&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;total_deployments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_interval_days&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;weekly_deployments&lt;/span&gt;

    &lt;span class="n"&gt;risk_level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CRITICAL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;average_gap_days&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HIGH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;     &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;average_gap_days&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MEDIUM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;average_gap_days&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LOW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;average_exposure_window_days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;average_gap_days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_exposure_window_days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;test_interval_days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;untested_deployments_per_cycle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;total_deployments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;risk_level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;risk_level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it against a team shipping three times a week on an annual test cadence and you get a 182.5-day average exposure window and 156 untested deployments per cycle: CRITICAL. Drop the interval to monthly and the same team lands at a 15-day average window and roughly 13 untested deployments: MEDIUM. The application didn't get safer. The window just got shorter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuous pentesting isn't "scan on every commit"
&lt;/h2&gt;

&lt;p&gt;Worth being precise here, because the term gets diluted fast: continuous penetration testing does not mean pointing an automated scanner at the app on every push and calling it a pentest. Scanners find known patterns at volume. A pentest investigates how a weakness can actually be chained and exploited in the context of a real, running application, with defined scope, authorization, controlled exploitation, and human oversight over the findings.&lt;/p&gt;

&lt;p&gt;What continuous testing does mean in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monthly testing&lt;/strong&gt; — a recurring assessment at a materially shorter interval than annual.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sprint-cadence testing&lt;/strong&gt; — assessments aligned to the development cycle itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Targeted testing&lt;/strong&gt; — auth changes, new APIs, payment flows, and infra changes trigger a focused assessment on top of the recurring one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrated coverage&lt;/strong&gt; — pentesting sits alongside IDE, PR, and CI/CD controls rather than replacing them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no single "correct" frequency. The right cadence is a function of deployment velocity, data sensitivity, regulatory obligation, and how fast the team can actually act on a finding once it exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoping a sprint by risk, not by calendar
&lt;/h2&gt;

&lt;p&gt;The more useful version of "test every sprint" is test every sprint &lt;em&gt;proportionally to what changed&lt;/em&gt;. A simple risk-scoring pass over merged PRs gets you most of the way there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SprintSecurityTestingProgram&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify_test_depth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;risk_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;risk_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FULL_DEPTH — auth changes require complete auth chain review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;risk_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TARGETED_DEEP — multiple security-relevant changes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;risk_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TARGETED_STANDARD — specific components need focused testing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LIGHTWEIGHT — automated testing sufficient&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;identify_priority_areas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;changed_components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;priorities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;changed_components&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authentication&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;priorities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;area&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authentication&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_focus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JWT validation, session management, MFA bypass, brute force&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;changed_components&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;priorities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;area&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_focus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RBAC, IDOR, cross-tenant access, role bypass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;changed_components&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data_access&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;priorities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;area&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data Access Layer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_focus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SQL/NoSQL injection, ownership filter presence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priorities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auth and authz changes get full-depth review every time. Everything else gets scoped by what it actually touches. That's the difference between a program that scales and one that turns into indiscriminate noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The economics people usually get wrong
&lt;/h2&gt;

&lt;p&gt;The lazy comparison is "one annual invoice" versus "a subscription." That comparison undercounts the annual model and overcounts the continuous one, because it ignores retesting, remediation effort, false-positive waste, emergency response, and the actual expected cost of a breach sitting inside a long exposure window.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost category&lt;/th&gt;
&lt;th&gt;Annual model&lt;/th&gt;
&lt;th&gt;Continuous model&lt;/th&gt;
&lt;th&gt;Delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct testing cost&lt;/td&gt;
&lt;td&gt;$25K–$50K&lt;/td&gt;
&lt;td&gt;$48K–$72K/yr (sub)&lt;/td&gt;
&lt;td&gt;+$10K–$25K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retest cost&lt;/td&gt;
&lt;td&gt;$8K–$15K&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;-$12K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineering remediation&lt;/td&gt;
&lt;td&gt;$19.2K (8 findings × 3 days)&lt;/td&gt;
&lt;td&gt;$14.4K (12 findings × 1.5 days)&lt;/td&gt;
&lt;td&gt;-$4.8K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False positive waste&lt;/td&gt;
&lt;td&gt;$9.6K (30% FP rate)&lt;/td&gt;
&lt;td&gt;$1.6K (5% FP rate)&lt;/td&gt;
&lt;td&gt;-$8K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emergency response&lt;/td&gt;
&lt;td&gt;$17.5K (35% probability)&lt;/td&gt;
&lt;td&gt;$4K (8% probability)&lt;/td&gt;
&lt;td&gt;-$13.5K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expected breach cost&lt;/td&gt;
&lt;td&gt;$24.7K (180-day window)&lt;/td&gt;
&lt;td&gt;$1.6K (14-day window)&lt;/td&gt;
&lt;td&gt;-$23K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total TCO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$104K&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$80K&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-$24K&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Illustrative numbers for a $10M ARR company shipping weekly, 15 engineers at $100/hr, 12% annual breach probability, $500K average breach cost, but the shape of the comparison holds more broadly: the direct testing line goes up, almost everything downstream of it goes down, and the breach-cost line is usually the biggest single swing because it scales directly with how long the exposure window is.&lt;/p&gt;

&lt;p&gt;The expected-breach-cost math is worth writing out, because it's the part most TCO comparisons skip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;exposure_window_days_continuous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;  &lt;span class="c1"&gt;# sprint cadence
&lt;/span&gt;&lt;span class="n"&gt;adjusted_breach_probability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;breach_probability_annual&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;exposure_window_days_continuous&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;expected_breach_cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adjusted_breach_probability&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;avg_breach_cost&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shrink the exposure window, shrink the probability term, shrink the expected cost. It's linear and it's easy to underweight if you're only looking at the invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking a cadence: a decision framework, not a rule
&lt;/h2&gt;

&lt;p&gt;Four questions, roughly in order of how much weight they should carry:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How often do you deploy?&lt;/strong&gt; Weekly or more and annual testing is covering under 10% of your deployments by construction. Monthly or less and annual/semi-annual is probably fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What data do you handle?&lt;/strong&gt; PII at scale, payment data, or health data pushes you toward quarterly-minimum or continuous regardless of deploy cadence, because breach impact and regulatory exposure are both high.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the regulatory environment?&lt;/strong&gt; PCI DSS, SOC 2, HIPAA, and ISO 27001 all have their own expectations for testing evidence and cadence. Continuous testing can supplement that evidence; it doesn't automatically substitute for a compliance-mandated assessment, and that distinction matters when an auditor is asking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you actually act on findings?&lt;/strong&gt; A one-person security team that can triage in real time can run continuous. A team with no dedicated security function and no plan for continuous will just accumulate an ignored backlog, which is worse than not having found the issues at all.&lt;/p&gt;

&lt;p&gt;Rough mapping:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Deployment frequency&lt;/th&gt;
&lt;th&gt;Recommended model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Less than monthly&lt;/td&gt;
&lt;td&gt;Annual / semi-annual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly to bi-weekly&lt;/td&gt;
&lt;td&gt;Quarterly minimum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly or more&lt;/td&gt;
&lt;td&gt;Sprint-based or continuous&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  SLAs are what keep a continuous program from collapsing
&lt;/h2&gt;

&lt;p&gt;Continuous testing produces continuous findings, and without a severity-tiered SLA, that turns into noise the engineering team eventually starts ignoring.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;CVSS&lt;/th&gt;
&lt;th&gt;Acknowledge&lt;/th&gt;
&lt;th&gt;Remediate&lt;/th&gt;
&lt;th&gt;Retest&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;td&gt;9.0–10.0&lt;/td&gt;
&lt;td&gt;4h&lt;/td&gt;
&lt;td&gt;48h&lt;/td&gt;
&lt;td&gt;within 24h of fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;7.0–8.9&lt;/td&gt;
&lt;td&gt;24h&lt;/td&gt;
&lt;td&gt;7d&lt;/td&gt;
&lt;td&gt;within 48h of fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;4.0–6.9&lt;/td&gt;
&lt;td&gt;72h&lt;/td&gt;
&lt;td&gt;30d&lt;/td&gt;
&lt;td&gt;within sprint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;0.1–3.9&lt;/td&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;td&gt;90d&lt;/td&gt;
&lt;td&gt;next quarterly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why continuous programs actually die
&lt;/h2&gt;

&lt;p&gt;Most don't fail because the testing was bad. They fail for one of four operational reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Finding fatigue without triage.&lt;/strong&gt; Everything gets flagged with the same urgency, engineers tune it out. Fix: CVSS-based SLAs, a security champion per team doing first-pass triage, a weekly standup instead of ad-hoc tickets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing is time-based instead of change-based.&lt;/strong&gt; A calendar cadence misses whatever shipped between checkpoints. Fix: scope each cycle off actual PR/change data, not the calendar (this is what the sprint risk-scoring class above is for).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surface monitoring without ownership.&lt;/strong&gt; New subdomains and endpoints get flagged and nobody owns the follow-up. Fix: a rotation that owns the alert queue, with an explicit SLA (72h is reasonable) for investigating new surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance-minimum thinking.&lt;/strong&gt; The program quietly reverts to "just enough for the auditor." Fix: report on breach-probability and incident-cost terms at the board level, not just pass/fail testing status, so the program's ROI stays visible.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Metrics that actually tell you if it's working
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Annual baseline&lt;/th&gt;
&lt;th&gt;Continuous target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mean time to detection&lt;/td&gt;
&lt;td&gt;~180 days&lt;/td&gt;
&lt;td&gt;&amp;lt;14 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean time to remediation&lt;/td&gt;
&lt;td&gt;~45 days (batch quarterly)&lt;/td&gt;
&lt;td&gt;&amp;lt;7 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vulnerability escape rate&lt;/td&gt;
&lt;td&gt;~25% (found by someone else first)&lt;/td&gt;
&lt;td&gt;&amp;lt;5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLA compliance rate&lt;/td&gt;
&lt;td&gt;~60%&lt;/td&gt;
&lt;td&gt;&amp;gt;95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False positive rate&lt;/td&gt;
&lt;td&gt;~40%&lt;/td&gt;
&lt;td&gt;&amp;lt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attack surface coverage&lt;/td&gt;
&lt;td&gt;~70%&lt;/td&gt;
&lt;td&gt;&amp;gt;95%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these should be optimized in isolation. The point of tracking them together is to see whether the gap between "change ships" and "change gets validated" is actually shrinking over time, not just whether a test happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI actually changes the equation
&lt;/h2&gt;

&lt;p&gt;The reason traditional pentest firms can't support a monthly cadence isn't unwillingness, it's structural: a human consultant works a target sequentially over one to two weeks, and that timeline doesn't compress just because you want it to.&lt;/p&gt;

&lt;p&gt;What changes the math is running large numbers of specialized exploit agents in parallel against a target instead of one consultant working through it linearly, and carrying codebase context forward between engagements instead of starting cold every time, which is closer to how an attacker who's already been inside your codebase (via source access, leaked credentials, or a prior foothold) would actually operate. That combination, source-aware testing plus accumulated context, is what makes a 48-hour full-engagement turnaround and unlimited free retesting operationally sustainable instead of a pricing gimmick.&lt;/p&gt;

&lt;p&gt;If you want to see how that model works end to end: &lt;a href="https://www.codeant.ai/" rel="noopener noreferrer"&gt;CodeAnt AI's continuous penetration testing platform&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual takeaway
&lt;/h2&gt;

&lt;p&gt;Annual pentesting isn't wrong. It's a point-in-time answer to a problem that, for most SaaS teams now, is continuous. The question worth asking isn't "did we pass our last pentest," it's "how long does a newly introduced risk sit untested before anyone looks at it." If that number is measured in months, the testing cadence and the deployment cadence have drifted apart, and that drift is the actual attack surface.&lt;/p&gt;

&lt;p&gt;For the cost-side deep dive behind the TCO numbers above, see &lt;a href="https://www.codeant.ai/blog/penetration-testing-cost" rel="noopener noreferrer"&gt;CodeAnt's guide to penetration testing costs&lt;/a&gt;. For the operational checklist on choosing a recurring testing vendor, see &lt;a href="https://www.codeant.ai/blog/ptaas-sla" rel="noopener noreferrer"&gt;PTaaS provider SLAs: what to look for&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>appsec</category>
      <category>devsecops</category>
      <category>pentesting</category>
    </item>
    <item>
      <title>How a GitHub Triage Role Hijacked an Already-Authorized Claude Code Action Run</title>
      <dc:creator>Amartya Jha</dc:creator>
      <pubDate>Mon, 31 Aug 2026 09:14:02 +0000</pubDate>
      <link>https://dev.to/codeant/how-a-github-triage-role-hijacked-an-already-authorized-claude-code-action-run-368j</link>
      <guid>https://dev.to/codeant/how-a-github-triage-role-hijacked-an-already-authorized-claude-code-action-run-368j</guid>
      <description>&lt;p&gt;&lt;em&gt;A GitHub collaborator with only the triage role could shift Claude Code Action's authorized trigger boundary and inject post-authorization input into a repository-writing run.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HackerOne #3918594 · Claude Code Action v1.0.185, commit &lt;code&gt;9db594c7a0e82298c121c18b7f08aa1579ce7341&lt;/code&gt; · CVSS 4.0 score 7.5, High&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Authorizing an automated job is really two promises: who gets to start it, and which data it's allowed to see once running. Most security reviews focus on the first and barely glance at the second, which is exactly where CodeAnt AI Security Research went looking in Claude Code Action, the GitHub bot that lets Claude respond to issues and pull requests and push changes back to a repository.&lt;/p&gt;

&lt;p&gt;What we found: a collaborator holding nothing more than GitHub's triage role, no write access, no ability to launch Claude directly, could wait for a maintainer to authorize a run and quietly shift that run's own idea of when it had been authorized, so the already-privileged job treated data the collaborator posted afterward as if it had always been there.&lt;/p&gt;

&lt;p&gt;This is not ordinary prompt injection. The data in question was created after the cutoff meant to exclude it, and it only got in because that cutoff itself was silently reassigned to a different person's action.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Claude Code Action is supposed to guarantee
&lt;/h2&gt;

&lt;p&gt;Two controls sit between a GitHub event and a repository-writing Claude run.&lt;/p&gt;

&lt;p&gt;The first is a write-permission gate: before Claude gets any repository-writing tools, the Action checks whether the actor behind the triggering webhook actually has write access to the repo.&lt;/p&gt;

&lt;p&gt;The second is a temporal cutoff: comments posted or edited after the trigger moment are filtered out of the context Claude sees, so nobody can sneak extra instructions in after a maintainer has already greenlit the run.&lt;/p&gt;

&lt;p&gt;Both controls sound airtight individually. We went looking for whether the actor the first control checks and the timestamp the second control uses actually describe the same underlying event. They didn't, at least not for one common trigger type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconstructing a timestamp from the wrong source
&lt;/h2&gt;

&lt;p&gt;For most trigger events, Claude Code Action reads its timestamp from the webhook payload that fired. &lt;code&gt;issues.labeled&lt;/code&gt; events work differently: the Action instead queries GitHub's API for the issue's full label history, filters to every event where a matching label was applied, and picks whichever happened most recently.&lt;/p&gt;

&lt;p&gt;The resolver, in essence, does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;labeled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;labelName&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;byCreatedAtDescending&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub's label-event data includes both an actor and a unique event identifier. Neither gets used here. The resolver treats "an event of this type, with this label name, that happened most recently" as if that were enough to identify one specific, authorized event. It isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two decisions, checking two different things
&lt;/h2&gt;

&lt;p&gt;Split this into the two questions the Action is actually answering.&lt;/p&gt;

&lt;p&gt;The first: is this actor allowed to start a write-capable run? Answered by checking &lt;code&gt;context.actor&lt;/code&gt; on the original webhook. A maintainer passes; a triage-level collaborator does not.&lt;/p&gt;

&lt;p&gt;The second: which repository context belongs to that now-authorized run? Answered, for label triggers, by a live query that ignores actor entirely and just grabs the newest matching event.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;permission identity: the original webhook's actor and event
content identity:    whichever live event matches type and label most recently
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those two answers are supposed to describe the same moment. Once they don't, anyone who fails the first question can still shape the outcome of the second, as long as they have enough GitHub permissions to produce a matching event of their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching the boundary actually move
&lt;/h2&gt;

&lt;p&gt;We built a deterministic timeline to prove this out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;09:59  A maintainer-authored task already exists on the issue
10:01  Maintainer applies the "claude" label
10:02  Triage-level collaborator comments RELEASE_APPROVAL=attacker-controlled
10:03  Same collaborator removes the "claude" label, then reapplies it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 10:01, when the run is supposed to be authorized, the 10:02 comment doesn't exist yet, so it has to stay out of scope no matter what.&lt;/p&gt;

&lt;p&gt;Run the resolver with only the original 10:01 event visible, and it correctly returns 10:01, comment excluded. Add the 10:03 relabel event back in, and the resolver instead returns 10:03. The exact same comment, unchanged, now reads as pre-trigger content and lands in the prompt.&lt;/p&gt;

&lt;p&gt;None of this required forging a webhook or impersonating the maintainer. The collaborator used only the ordinary label-management permissions GitHub already grants that role. The authorized job did the substitution itself, by trusting a live, mutable history over the event it was actually triggered by.&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%2Fvod9kd71unrvxmgum1lf.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%2Fvod9kd71unrvxmgum1lf.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What was and wasn't the trusted boundary
&lt;/h2&gt;

&lt;p&gt;To be clear: issue comments were never treated as trusted input, they're expected to be arbitrary and untrusted. Triage collaborators managing labels isn't a flaw either, GitHub deliberately gives that role exactly that capability.&lt;/p&gt;

&lt;p&gt;The actual protected boundary is narrower: the snapshot of context that existed at the moment a maintainer's authorization fired. Claude Code Action explicitly filters out anything added after that snapshot so a privileged run's inputs can't shift after the fact.&lt;/p&gt;

&lt;p&gt;That distinction, control over post-authorization input rather than simple access to untrusted content, is what separates this from a garden-variety prompt injection report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Following the mutation all the way to a commit
&lt;/h2&gt;

&lt;p&gt;We split verification into two independent layers so neither claim depended on the other.&lt;/p&gt;

&lt;p&gt;The first layer isolated the resolver and prompt-building logic itself: &lt;code&gt;resolveTriggerTimestamp()&lt;/code&gt;, &lt;code&gt;fetchGitHubData()&lt;/code&gt;, the permission check, and prompt generation, fed scripted responses standing in for a maintainer and a triage-level actor. It confirmed the maintainer's webhook passes the gate cleanly, the triage actor's own webhook fails it every time, and only a later same-label event from the triage actor moves the cutoff.&lt;/p&gt;

&lt;p&gt;The second layer went further, into the Action's bundled runtime and its real tag-mode tool access: edit, commit, push, loopback-only, against a local bare repository. The issue held an ordinary maintainer task: take the latest &lt;code&gt;RELEASE_APPROVAL&lt;/code&gt; value, write it to a file, commit and push it. The attacker's comment held nothing but a plain value, no jailbreak language. Wherever the rebinding took effect, Claude wrote, committed, and pushed that value; wherever it didn't, the value was simply absent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolating exactly what changed
&lt;/h2&gt;

&lt;p&gt;We designed five controls so each one changed a single security-relevant variable at a time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Permission check&lt;/th&gt;
&lt;th&gt;Cutoff selected&lt;/th&gt;
&lt;th&gt;Attacker's value&lt;/th&gt;
&lt;th&gt;Repository sink&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Triage actor triggers directly&lt;/td&gt;
&lt;td&gt;Rejected&lt;/td&gt;
&lt;td&gt;Never reached&lt;/td&gt;
&lt;td&gt;Never reached&lt;/td&gt;
&lt;td&gt;No write occurs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only the maintainer's original event exists&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;10:01&lt;/td&gt;
&lt;td&gt;Excluded&lt;/td&gt;
&lt;td&gt;No attacker-chosen file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A later event with a different label exists&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;10:01&lt;/td&gt;
&lt;td&gt;Excluded&lt;/td&gt;
&lt;td&gt;No attacker-chosen file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A later same-label event from the triage actor exists&lt;/td&gt;
&lt;td&gt;Allowed, under the maintainer's webhook&lt;/td&gt;
&lt;td&gt;10:03&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;File committed and pushed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same later event, but actor binding is enforced&lt;/td&gt;
&lt;td&gt;Allowed, under the maintainer's webhook&lt;/td&gt;
&lt;td&gt;10:01&lt;/td&gt;
&lt;td&gt;Excluded&lt;/td&gt;
&lt;td&gt;No attacker-chosen file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every case reproduced across three clean runs each. Notably, the sink-level proof recorded zero permission denials the entire time, because as far as the Action's own logic was concerned, it was still faithfully executing the maintainer's original, authorized job.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a repository actually loses
&lt;/h2&gt;

&lt;p&gt;Any repository using &lt;code&gt;issues.labeled&lt;/code&gt; with a Claude trigger label, combined with a write-capable workflow token, was exposed to having its authorized job's inputs quietly mutated by a collaborator well below write access.&lt;/p&gt;

&lt;p&gt;Depending on the maintainer-authored task, admitted data could shape generated source or config, release metadata, the content of Claude's commits, which branches got pushed, and anything downstream that trusted those commits without a second look.&lt;/p&gt;

&lt;p&gt;We rated this High rather than Critical: exploitation needs a label-triggered workflow, a collaborator with triage permissions, a timing window before the authorized job finishes its live lookup, and a maintainer task where the admitted data meaningfully changes the output.&lt;/p&gt;

&lt;p&gt;Anthropic's assigned score reflects that conditionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The actual root cause
&lt;/h2&gt;

&lt;p&gt;Underneath the GitHub-specific details, this is a case of mutable semantic attributes standing in for a real event identity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;what should have been bound: the webhook's own, immutable event
what actually got bound:     the newest live event sharing type and label name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;event === "labeled"&lt;/code&gt; and &lt;code&gt;label.name === "claude"&lt;/code&gt; describe a whole class of events, not one specific event. Picking the newest member of that class makes the boundary sensitive to anything that happens after the original authorization, as long as it matches type and label.&lt;/p&gt;

&lt;p&gt;The permission check was correct the moment it ran. The comment filter was correct for whatever timestamp it was handed. The failure lived entirely in the handoff between those two steps, where the timestamp's actual origin got lost.&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%2F5lrje9up3fveq9mo4wds.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%2F5lrje9up3fveq9mo4wds.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How we'd fix it
&lt;/h2&gt;

&lt;p&gt;The durable solution carries one authenticated event identity, unbroken, from authorization through context collection through execution.&lt;/p&gt;

&lt;p&gt;Where GitHub exposes a trusted, immutable delivery identifier, use that instead of reconstructing anything from live history.&lt;/p&gt;

&lt;p&gt;Where a live lookup is unavoidable, require the selected event's actor to match the original webhook's actor, anchor candidates to a job-receipt timestamp so nothing afterward can drag the cutoff forward, and fail closed on ambiguity.&lt;/p&gt;

&lt;p&gt;Preserve the original webhook payload for trigger-time state, and add regression coverage for a different actor relabeling the issue mid-run.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for agentic CI more broadly
&lt;/h2&gt;

&lt;p&gt;Authorizing a job isn't only a decision about who may start it. It's also, implicitly, a decision about which inputs it's allowed to consume, and if that snapshot can shift after approval, the authorization can be correct while the resulting behavior isn't.&lt;/p&gt;

&lt;p&gt;Two events sharing an action type and a label name are not the same event; they can belong to different actors and trust levels even when every field looks identical.&lt;/p&gt;

&lt;p&gt;A role check reviewed in isolation ("can this role trigger the workflow") misses the more useful question: what can a role that fails it still influence once a stronger role has already passed.&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%2Fxftqxj0ip6ermkxykvqb.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%2Fxftqxj0ip6ermkxykvqb.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure timeline
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;August 5, 2026&lt;/td&gt;
&lt;td&gt;Report submitted to Anthropic through HackerOne (#3918594)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;August 13, 2026&lt;/td&gt;
&lt;td&gt;Anthropic validated the issue, assigned High severity, CVSS 4.0 score 7.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;August 14, 2026&lt;/td&gt;
&lt;td&gt;Bounty of $XXXX awarded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;August 24, 2026&lt;/td&gt;
&lt;td&gt;Editorial cutoff; report still triaged and private, no confirmed fixed version&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;Start by seeing whether your workflow even uses this trigger pattern:&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="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"anthropics/claude-code-action|issues.*labeled|types:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\?&lt;/span&gt;&lt;span class="s2"&gt;labeled"&lt;/span&gt; .github/workflows/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're in scope if your workflow triggers on &lt;code&gt;issues.labeled&lt;/code&gt; with a Claude trigger label, runs in tag mode or otherwise grants workspace edits plus commit and authenticated push, and hands the job a write-capable token.&lt;/p&gt;

&lt;p&gt;Anyone with GitHub's triage role, or any other sub-write role, can add and remove labels freely without writing code, generating the newer same-label event this bug depends on.&lt;/p&gt;

&lt;p&gt;Until a fixed version is confirmed, restrict who can apply your Claude trigger label to write-and-above roles, treat anything a label-triggered run commits or pushes as unreviewed until a human looks at it, and keep downstream CI or deploy steps from auto-consuming those commits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves things
&lt;/h2&gt;

&lt;p&gt;Claude Code Action's write-permission gate worked exactly as intended: it rejected the triage collaborator's own attempts to start a run, every time. The vulnerability sat one layer deeper: an already-authorized job later reconstructed its own trigger boundary from mutable history, and accepted a newer event belonging to the very principal it had just rejected.&lt;/p&gt;

&lt;p&gt;The invariant here is simple to state and easy to violate: the event that defines a privileged agent's input boundary has to be the same event that authorized the run. Keep that identity intact, and later activity, however legitimate on its own, can't quietly rewrite an earlier authorization decision.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was originally published by &lt;a href="https://codeant.ai/security-research/claude-code-action-post-authorization-injection" rel="noopener noreferrer"&gt;CodeAnt AI's Security Research Team&lt;/a&gt;. Read the complete breakdown here →&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>github</category>
      <category>claude</category>
      <category>ai</category>
    </item>
    <item>
      <title>Escaping Claude Code's Sandbox: A TOCTOU Bug That Let Repo Code Overwrite Host Files</title>
      <dc:creator>Amartya Jha</dc:creator>
      <pubDate>Mon, 31 Aug 2026 09:08:39 +0000</pubDate>
      <link>https://dev.to/codeant/escaping-claude-codes-sandbox-a-toctou-bug-that-let-repo-code-overwrite-host-files-50hh</link>
      <guid>https://dev.to/codeant/escaping-claude-codes-sandbox-a-toctou-bug-that-let-repo-code-overwrite-host-files-50hh</guid>
      <description>&lt;p&gt;&lt;em&gt;A parent-directory swap timed against a permission check let sandboxed repo code redirect a host-owned file write. CVSS 7.7, High.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HackerOne #3882177 · Claude Code 2.1.217, macOS arm64 · CVSS 4.0 score 7.7, High&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Sandboxing runs on a simple promise: code inside it cannot reach outside it. Claude Code, Anthropic's coding agent, keeps that promise on macOS by boxing in Bash and everything it spawns, while leaving one door unlocked: a trusted editor process that applies the file changes Claude has permission to make.&lt;/p&gt;

&lt;p&gt;We at CodeAnt AI Security Research tested whether that door could be reached from the wrong side of the sandbox. It could. A process confined entirely to the Bash sandbox got the trusted, host-owned Edit tool to overwrite a file the sandbox had already been denied direct access to. No outside-write approval appeared anywhere, and no permission check failed. The file simply changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two kinds of trust sharing one filesystem
&lt;/h2&gt;

&lt;p&gt;Claude Code splits authority during a session. Bash commands and their children run inside an OS-level sandbox with restricted filesystem access, so a repository's tests and builds can execute without full run of your machine. Built-in tools like Edit run in the trusted host process, the part actually allowed to touch your project.&lt;/p&gt;

&lt;p&gt;Permissions decide whether Claude may call a tool at all. Sandboxing then restricts what Bash and its descendants can do at the OS level regardless of permission. In &lt;code&gt;acceptEdits&lt;/code&gt; mode, edits auto-approve only inside the working directory; Seatbelt enforces that boundary for sandboxed processes, which is why a direct outside write returns &lt;code&gt;EPERM&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;None of that is the problem. The problem sits one layer up: the assumption that the file a permission check looked at is still the file the trusted writer touches a moment later.&lt;/p&gt;

&lt;h2&gt;
  
  
  A file path is a set of directions, not a destination
&lt;/h2&gt;

&lt;p&gt;A path like &lt;code&gt;/workspace/project/config.json&lt;/code&gt; isn't a pointer to one fixed object. It's a sequence of instructions: enter this directory, then this one, then open this file. Every step resolves fresh, at the moment something reads it.&lt;/p&gt;

&lt;p&gt;Change one directory partway through that walk, without touching the final filename, and you change where the whole path ends up. Turn &lt;code&gt;project&lt;/code&gt; into a symlink pointing elsewhere, and the same string now means something entirely different.&lt;/p&gt;

&lt;p&gt;That's a time-of-check-to-time-of-use problem (TOCTOU for short): a gap between when something is verified and when it's used, wide enough for the underlying object to change in between. Claude Code's Edit tool had exactly that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the two checks stopped agreeing
&lt;/h2&gt;

&lt;p&gt;Static analysis of Claude Code 2.1.217's embedded JavaScript showed how Edit authorizes and writes a file, in equivalent pseudocode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;checkParentResolutions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;replaceFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool checks the target's parent directories, checks again to catch ordinary symlink swaps, creates any missing directories, then writes the file.&lt;/p&gt;

&lt;p&gt;That second check narrows the window for simple symlink substitution but doesn't close it. It only compares path strings at one instant, never binding authorization to a stable directory handle or inode. And the recursive &lt;code&gt;mkdir&lt;/code&gt; call, which takes real wall-clock time, runs after that last check and before the write. That's the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning an ordinary developer workflow into the trigger
&lt;/h2&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%2F3yz92qi5trnvc1xre5cv.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%2F3yz92qi5trnvc1xre5cv.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We didn't want to assume a repository could hand-pick an arbitrary Edit call, so we used a sequence any Claude Code user would recognize as normal:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A developer opens a repository they don't fully control.&lt;/li&gt;
&lt;li&gt;They ask Claude to run the tests and fix whatever's failing.&lt;/li&gt;
&lt;li&gt;Claude runs the repo's own test command inside the sandbox, exactly as designed.&lt;/li&gt;
&lt;li&gt;A pretest step quietly launches a small helper process, still confined to the sandbox, and has it wait.&lt;/li&gt;
&lt;li&gt;The test fails, reporting a missing fixture at a path that looks entirely ordinary.&lt;/li&gt;
&lt;li&gt;Claude, being helpful, creates that fixture using its built-in Edit path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The repository controls the test, the failure message, and the fixture path. It never has to guess what Claude would independently choose.&lt;/p&gt;

&lt;p&gt;A simplified target: &lt;code&gt;&amp;lt;workspace&amp;gt;/slot/signal/private/tmp/demo/outside/payload.txt&lt;/code&gt;. &lt;code&gt;signal&lt;/code&gt; starts as an ordinary directory; next to it sits &lt;code&gt;armed&lt;/code&gt;, already a symlink to &lt;code&gt;/&lt;/code&gt;. The helper never leaves the sandbox: a direct outside write gets &lt;code&gt;EPERM&lt;/code&gt; every time. Its only useful move: renaming two entries it already owns inside the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The swap itself
&lt;/h2&gt;

&lt;p&gt;The helper needed to know exactly when to move. The signal is the &lt;code&gt;mkdir&lt;/code&gt; call itself: the moment the first missing directory gets created, Claude Code's final resolution check has already run and passed.&lt;/p&gt;

&lt;p&gt;At that instant it calls &lt;code&gt;renameatx_np&lt;/code&gt; with &lt;code&gt;RENAME_SWAP&lt;/code&gt;, exchanging two directory entries atomically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before: signal = ordinary directory, armed = symlink to /
after:  signal = symlink to /, armed = ordinary directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fmh7gr11plz681z589fqz.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%2Fmh7gr11plz681z589fqz.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The untouched, textually identical target path now resolves somewhere else entirely. Effective target: &lt;code&gt;/private/tmp/demo/outside/payload.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Claude Code's host-side writer, unrestricted by the sandbox, finishes exactly as it was going to: creates a temp file, renames it over the target. It has no reason to suspect anything changed. The sandboxed helper never touched the outside file itself. It only flipped an entry it already owned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Only the last step of the path was actually guarded
&lt;/h2&gt;

&lt;p&gt;Claude Code's writer does refuse to follow a symlink in the final path component, a genuine defense that stops a simpler attack cold.&lt;/p&gt;

&lt;p&gt;It doesn't help here, because this race happens on an ancestor directory, not the final component. Guarding only the last link in the chain does nothing to guarantee the earlier links stayed the same object they were when permission was granted.&lt;/p&gt;

&lt;p&gt;The permission system reasoned about a string and a resolution it observed once. The writer, later, walked that same string again and got a different answer. That mismatch is the entire vulnerability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving it beyond a single lucky run
&lt;/h2&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%2F4fbh99jwycu55xz836su.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%2F4fbh99jwycu55xz836su.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We built the reproduction to leave zero ambiguity about which boundary was crossed: isolated canary files, a throwaway home directory, a deterministic local model fixture, and no Anthropic credentials or live services involved.&lt;/p&gt;

&lt;p&gt;Five variations, each changing exactly one property, separated the bug from expected behavior. Each ran three consecutive times, clean every time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;What it proves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct &lt;code&gt;Edit&lt;/code&gt; call to a path outside the workspace&lt;/td&gt;
&lt;td&gt;Denied&lt;/td&gt;
&lt;td&gt;The permission layer correctly recognizes outside writes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sandboxed helper tries writing the outside canary directly&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EPERM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Seatbelt correctly blocks the attacker from the real target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same directory layout, swap never triggered&lt;/td&gt;
&lt;td&gt;Canary untouched&lt;/td&gt;
&lt;td&gt;The path is safe as long as object identity doesn't change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swap timed against the &lt;code&gt;mkdir&lt;/code&gt; signal&lt;/td&gt;
&lt;td&gt;Canary overwritten, zero permission denials logged&lt;/td&gt;
&lt;td&gt;The host writer crossed the workspace boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A sibling dev project watched by an autoreloader&lt;/td&gt;
&lt;td&gt;Modified source executed&lt;/td&gt;
&lt;td&gt;An ordinary downstream tool turns the overwrite into execution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What this actually gets an attacker
&lt;/h2&gt;

&lt;p&gt;With this primitive, a malicious repository, a compromised dependency, or a prompt-injected command running inside the Bash sandbox could get the trusted host process to overwrite an attacker-chosen, user-writable file anywhere outside the workspace: modifying sibling projects, tampering with build configuration, corrupting files CI reads downstream, and in some cases running arbitrary code, when something already present picks up the changed file on its own.&lt;/p&gt;

&lt;p&gt;To be precise: this isn't an unconditional remote-code-execution bug. Getting from "overwritten" to "executed" needs an existing consumer already watching that location. On its own, the proven impact is arbitrary modification of a file outside the authorized workspace.&lt;/p&gt;

&lt;p&gt;Anthropic rated the report High, CVSS 4.0 score 7.7:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why checking twice wasn't enough
&lt;/h2&gt;

&lt;p&gt;The root cause is an authorization-to-use binding failure: the identity authorized at time T1 isn't the identity actually used at time T2. Rechecking the path narrows that gap but can't close it, since any later step that takes real time hands an attacker another window.&lt;/p&gt;

&lt;p&gt;We also found the identical check-then-mkdir-then-write ordering in Claude Code's Write tool during static review. We're flagging the pattern, not claiming a second confirmed exploit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing it properly
&lt;/h2&gt;

&lt;p&gt;The durable fix ties authorization and the eventual write to the same filesystem objects, not to a string re-resolved twice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the trusted workspace root once and hold its descriptor for the whole operation.&lt;/li&gt;
&lt;li&gt;Walk every ancestor relative to that descriptor using &lt;code&gt;openat&lt;/code&gt;-style calls, never by re-resolving the full path.&lt;/li&gt;
&lt;li&gt;Open each directory with &lt;code&gt;O_DIRECTORY | O_NOFOLLOW&lt;/code&gt;, keeping descriptors alive until the write completes.&lt;/li&gt;
&lt;li&gt;Create missing directories with &lt;code&gt;mkdirat&lt;/code&gt;, relative to an already-verified parent.&lt;/li&gt;
&lt;li&gt;Perform the final write with &lt;code&gt;openat&lt;/code&gt; and &lt;code&gt;renameat&lt;/code&gt;, not plain pathname operations.&lt;/li&gt;
&lt;li&gt;If a component must be reopened across an async gap, verify its device and inode identity first.&lt;/li&gt;
&lt;li&gt;Apply the same pattern everywhere this boundary shows up: Edit, Write, and any shared helper.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A pathname recheck before the final write is still worth having as defense in depth. It just can't be the only defense.&lt;/p&gt;

&lt;h2&gt;
  
  
  The broader lesson for agentic tools
&lt;/h2&gt;

&lt;p&gt;Sandboxing a process doesn't remove its ability to influence what a privileged process does next. It can still shape diagnostics, file paths, and filesystem state the trusted side later consumes.&lt;/p&gt;

&lt;p&gt;A pathname is not a durable identity; it's a recipe re-executed on demand. Any system that authorizes based on one resolution and acts on a later one needs to preserve object identity across that gap, not just recheck the string.&lt;/p&gt;

&lt;p&gt;The strongest proof here was the contrast between a write that returned &lt;code&gt;EPERM&lt;/code&gt; and a write, moments later, from the trusted side, that succeeded against the same target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you worry about this right now
&lt;/h2&gt;

&lt;p&gt;Check your version first: &lt;code&gt;claude --version&lt;/code&gt;. This finding was reproduced on Claude Code 2.1.217, macOS arm64. Confirm affected and fixed ranges directly with Anthropic.&lt;/p&gt;

&lt;p&gt;You're in the risk zone if: you're on macOS, you let Claude run repository commands in the Bash sandbox while using the built-in Edit/Write tools, you run in &lt;code&gt;acceptEdits&lt;/code&gt; mode, and you regularly open repositories you don't fully trust.&lt;/p&gt;

&lt;p&gt;Until a fixed build is confirmed, prefer explicit per-edit approval over &lt;code&gt;acceptEdits&lt;/code&gt; for code you don't control, and watch for sibling projects or build watchers running alongside your workspace, since that's what turns an overwrite into execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The invariant this bug violated is easy to state and hard to implement everywhere it needs to hold: the object that receives a write has to be the same object, reached through the same verified chain of ancestors, that the permission system actually authorized.&lt;/p&gt;

&lt;p&gt;If a system checks a path once and follows that same path again later, an attacker who can move what's underneath the path (without touching the string itself) can walk past every check while each one still reports success.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was originally published by &lt;a href="https://codeant.ai/security-research/claude-code-macos-parent-directory-toctou-vulnerability" rel="noopener noreferrer"&gt;CodeAnt AI's Security Research Team&lt;/a&gt;. Read the complete breakdown here →&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>claude</category>
      <category>sandbox</category>
      <category>macos</category>
    </item>
    <item>
      <title>CVE-2026-71511 · Read a member's card, receive their password hash</title>
      <dc:creator>Amartya Jha</dc:creator>
      <pubDate>Tue, 25 Aug 2026 09:20:02 +0000</pubDate>
      <link>https://dev.to/codeant/cve-2026-71511-read-a-members-card-receive-their-password-hash-1lcl</link>
      <guid>https://dev.to/codeant/cve-2026-71511-read-a-members-card-receive-their-password-hash-1lcl</guid>
      <description>&lt;p&gt;&lt;strong&gt;CVSS 6.5 · Sensitive data exposure (CWE-200)&lt;/strong&gt; · Fixed in Dolibarr 24.0.0&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every reply is supposed to pass a redactor that blacks out secrets. The Users redactor blacks out the password. The Members redactor, same job, next desk, forgot that one line, and hands the password verifier out with the card.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The attack in six steps&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every API reply runs through a "cleaner"&lt;/strong&gt;. Its job is to strip secrets before anything is sent back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line the two cleaners up side by side&lt;/strong&gt;. The one for users against the one for members. &lt;em&gt;(this is the break)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Users cleaner removes the stored password hash&lt;/strong&gt;. Exactly as it should.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Members cleaner doesn't&lt;/strong&gt;. One missing line, doing the identical job one desk over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A "read members" key receives every member's password verifier&lt;/strong&gt;. And from the list route, all of them at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirmed live, not a placeholder&lt;/strong&gt;. Matched to the database, and it validated the real password. &lt;strong&gt;→ credential exposure.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before an application sends any record back to a caller, something has to decide what to leave out. Passwords, keys, the private machinery of an account, all of it is supposed to be stripped on the way out, by a step whose only job is redaction. When that step is thorough, you never think about it. When it isn't, secrets walk out in plain sight.&lt;/p&gt;

&lt;p&gt;This is the story of two of those redactors, doing the same job for two similar kinds of record, where one remembered to black out the password and the other simply didn't, and how that one missing line handed every member's password verifier to anyone allowed to read a member card.&lt;/p&gt;

&lt;p&gt;It is a simple example of how sensitive data exposure can happen inside an otherwise legitimate API response. The endpoint itself works as designed. The problem is that the Dolibarr security check responsible for removing sensitive credential data was incomplete.&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%2F354tiq8jv0eewrm6lp27.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%2F354tiq8jv0eewrm6lp27.png" alt=" " width="800" height="504"&gt;&lt;/a&gt;&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%2Funqmhw52hsfhebagxvtr.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%2Funqmhw52hsfhebagxvtr.png" alt=" " width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Found by holding two things side by side
&lt;/h2&gt;

&lt;p&gt;This finding didn't come from a clever attack. It came from comparison. Dolibarr redacts a &lt;em&gt;user&lt;/em&gt; record and a &lt;em&gt;member&lt;/em&gt; record with two separate pieces of code meant to do the same thing. So we put them next to each other and read them line for line.&lt;/p&gt;

&lt;p&gt;The user redactor removed the stored password verifier before sending, exactly right. The member redactor, doing the identical job one desk over, didn't. One missing line, in one of two near-twin functions.&lt;/p&gt;

&lt;p&gt;This is the kind of issue that can be missed by a straightforward application security assessment. Nothing needs to be injected or bypassed in the traditional sense. The vulnerability appears when two pieces of code responsible for protecting the same sensitive data are compared and found to enforce different rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that one line leaks
&lt;/h2&gt;

&lt;p&gt;The consequence is not subtle. An account holding nothing more than the right to read members, an ordinary, widely-granted permission, receives, along with each member's ordinary details, the stored verifier for that member's password. And because the same gap sits on the route that lists &lt;em&gt;all&lt;/em&gt; members, you don't get them one at a time. You get the whole set in a single reply.&lt;/p&gt;

&lt;p&gt;We confirmed the value was the real thing rather than a harmless placeholder by matching it back to the database and checking it corresponded to the member's actual password. It was live.&lt;/p&gt;

&lt;p&gt;For a security researcher or penetration tester, that distinction matters. This was not simply a field that looked sensitive in a source-code review. We verified that the API returned real credential material and traced it back to the underlying application data.&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%2Fp2xove8jz3r29sfp4g5q.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%2Fp2xove8jz3r29sfp4g5q.png" alt=" " width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole pattern in one line
&lt;/h2&gt;

&lt;p&gt;If you want the entire theme of this research compressed into a single example, it is this one. The safe way already existed. The correct redactor was written, working, and sitting a file away. The neighbouring code that did the same job just skipped it. Not a hard problem, not a clever oversight, just a copy that wasn't quite a copy.&lt;/p&gt;

&lt;p&gt;That is what “split-brain authorization” looks like at its smallest: two places responsible for the same rule, quietly disagreeing, with no one comparing them until someone does.&lt;/p&gt;

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

&lt;p&gt;The fix in &lt;strong&gt;24.0.0&lt;/strong&gt; brings the member redactor in line with the user one, so the stored password material is stripped before any member record is sent, from single-record and list replies alike.&lt;/p&gt;

&lt;p&gt;The lesson is almost embarrassingly practical: when two parts of a codebase are supposed to enforce the same rule, the highest-value test is often not an exploit at all. It is reading them side by side and asking whether they actually match. One of them got it right. Diffing it against its neighbour is how you find the one that didn't.&lt;/p&gt;

&lt;p&gt;This is also why effective application penetration testing combines testing the running application with understanding how security controls are implemented. A response can look perfectly normal while still exposing sensitive data if one authorization or redaction path behaves differently from its counterpart.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The fix.&lt;/strong&gt; Upgrade to Dolibarr 24.0.0. That closes this finding. Want to know where your own app disagrees with itself? &lt;a href="https://codeant.ai/pentesting" rel="noopener noreferrer"&gt;→ Start with a free CodeAnt pentest&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/codeant/the-back-office-break-in-nine-dolibarr-findings-5cp8"&gt;← Back to all nine findings&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>security</category>
      <category>opensource</category>
      <category>penetrationtesting</category>
    </item>
    <item>
      <title>CVE-2026-71510 · Ask enough yes/no questions and you know everyone's salary</title>
      <dc:creator>Amartya Jha</dc:creator>
      <pubDate>Tue, 25 Aug 2026 09:18:01 +0000</pubDate>
      <link>https://dev.to/codeant/cve-2026-71510-ask-enough-yesno-questions-and-you-know-everyones-salary-3ja9</link>
      <guid>https://dev.to/codeant/cve-2026-71510-ask-enough-yesno-questions-and-you-know-everyones-salary-3ja9</guid>
      <description>&lt;p&gt;&lt;strong&gt;CVSS 6.5 · Authorization oracle → blind disclosure (CWE-863 / CWE-200)&lt;/strong&gt; · Fixed in Dolibarr 24.0.0&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You can't see the salary column, but the search box will happily answer “is this person's salary above 50,000? yes or no.” Ask twenty smart questions and you've read the exact number without ever seeing it.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The attack in six steps&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The user list has a search filter&lt;/strong&gt;: you can narrow it by a column's value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's safe from SQL injection&lt;/strong&gt;: the obvious attack was checked, and it holds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The unasked question: which columns may you filter on?&lt;/strong&gt;: the answer was any of them, including salary and the password hash. &lt;em&gt;(this is the break)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Those columns are stripped from the reply&lt;/strong&gt;: so you never actually see the value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;But "filter salary above X" returns a row, or nothing&lt;/strong&gt;: that single yes/no is a leak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary-search the answers to the exact number&lt;/strong&gt;: salaries recovered blind, plus a case-folded image of the hash. &lt;strong&gt;→ blind disclosure.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is a whole class of secret you can steal without ever being shown it. You don't need the system to &lt;em&gt;hand&lt;/em&gt; you the value. You only need it to answer yes-or-no questions about it, honestly, as many times as you like. Twenty good questions are enough to pin down almost any number.&lt;/p&gt;

&lt;p&gt;This is the story of a search box that would answer exactly those questions about columns it was supposed to keep hidden, salaries, and even the stored password verifier, and how a low-privilege account read them precisely, blind, without the system ever displaying a single one.&lt;/p&gt;

&lt;p&gt;It is the most subtle finding in the set, because nothing here looks broken. No error, no leak on the screen, no obvious hole. Just a search filter answering a question it should have refused to consider.&lt;/p&gt;

&lt;p&gt;This is the kind of web application security issue that can slip past a straightforward vulnerability scan: the search feature is not injectable, the response does not expose the sensitive field, but the application's authorization logic still lets a low-privilege user interrogate 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%2F3yewm67dllirtodk5q5x.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%2F3yewm67dllirtodk5q5x.png" alt=" "&gt;&lt;/a&gt;&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%2Fvflvmu79tgz7xxkk27dr.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%2Fvflvmu79tgz7xxkk27dr.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The obvious attack, and the one underneath it
&lt;/h2&gt;

&lt;p&gt;The user list has a search filter, so you can narrow the list by a column's value. The reflex is to test it for injection: can you break out of the query and run your own? We checked, and the answer was no. On that front it is safe, and a lot of reviews would stop there satisfied.&lt;/p&gt;

&lt;p&gt;The second, quieter question is not about breaking the query at all. It is about authorization: &lt;em&gt;which columns are you even allowed to filter on?&lt;/em&gt; The answer turned out to be all of them, including the salary fields and the password verifier that the results are so careful never to display.&lt;/p&gt;

&lt;p&gt;This is where security testing has to go beyond checking whether an endpoint returns the expected response. A penetration test also needs to ask whether a low-privilege user can use an otherwise legitimate feature to access information they are not authorized to interrogate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning a filter into a yes/no oracle
&lt;/h2&gt;

&lt;p&gt;Those columns are stripped from what comes back, so you never &lt;em&gt;see&lt;/em&gt; a salary in the list. But you can still filter on one. And a filter you can apply to a hidden column is a question the system will answer: ask it to show everyone whose salary is above fifty thousand, and either a matching row comes back or it doesn't. Row or no row. Yes or no.&lt;/p&gt;

&lt;p&gt;That single bit is the whole leak. Ask “above 50,000?”: no. “above 25,000?”: yes. “above 37,000?”: and so on, each question halving the range that's left. In a couple of dozen questions you have squeezed the exact figure out of a value the screen never showed you. Point the same technique at the stored password verifier and you can walk it out piece by piece.&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%2Fab8nlqhgvrefvqbdrnla.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%2Fab8nlqhgvrefvqbdrnla.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part: what it is not
&lt;/h2&gt;

&lt;p&gt;A responsible finding has to say where the danger stops, and we were careful to. The password material you can recover this way is not a usable key to log in with: Dolibarr stores it in a form it will not accept back as a login, and the recovered image is case-folded rather than exact. So this is a serious &lt;em&gt;disclosure&lt;/em&gt;, real salaries, real hidden fields, read blind, but it is not, by itself, account takeover. We tested that boundary, confirmed it held, and wrote the disproof into the report so a reviewer could see we had actually checked rather than assumed.&lt;/p&gt;

&lt;p&gt;That distinction matters for trust. It is easy to inflate a finding by implying the worst. It is more useful, and more credible, to show exactly how far it goes and exactly where it stops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's easy to miss, and what closes it
&lt;/h2&gt;

&lt;p&gt;This is the kind of hole endpoint-by-endpoint testing walks straight past. The filter isn't injectable. The response doesn't leak. Nothing throws an error. The only thing wrong is that a question was allowed which never should have been, and you only see it if you ask, of a search feature, not “can I break this,” but “which fields am I permitted to ask about.”&lt;/p&gt;

&lt;p&gt;The fix in &lt;strong&gt;24.0.0&lt;/strong&gt; restricts which columns the filter will accept, so hidden fields can no longer be interrogated through the back door of a yes/no search. The lesson is to treat a filter as an authorization surface, not just an injection surface: every column you let a caller filter on is a column you are willing to answer questions about, one bit at a time.&lt;/p&gt;

&lt;p&gt;This is also why security research needs to look beyond individual endpoints. In this case, the issue was not an obvious injection vulnerability. It was the interaction between filtering, authorization and sensitive fields, which is exactly the kind of behaviour that deeper application security testing can uncover.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The fix.&lt;/strong&gt; Upgrade to Dolibarr 24.0.0. That closes this finding. Want to know where your own app disagrees with itself? &lt;a href="https://dev.to/codeant/the-back-office-break-in-nine-dolibarr-findings-5cp8"&gt;→ Start with a free CodeAnt pentest&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/codeant/the-back-office-break-in-nine-dolibarr-findings-5cp8"&gt;← Back to all nine findings&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>opensource</category>
      <category>penetrationtesting</category>
    </item>
    <item>
      <title>CVE-2026-71509 · Approve your own expenses — and your whole team's</title>
      <dc:creator>Amartya Jha</dc:creator>
      <pubDate>Tue, 25 Aug 2026 09:16:07 +0000</pubDate>
      <link>https://dev.to/codeant/cve-2026-71509-approve-your-own-expenses-and-your-whole-teams-1fbl</link>
      <guid>https://dev.to/codeant/cve-2026-71509-approve-your-own-expenses-and-your-whole-teams-1fbl</guid>
      <description>&lt;p&gt;&lt;strong&gt;CVSS 6.5 · Approval / access-control bypass (CWE-862)&lt;/strong&gt; · Fixed in Dolibarr 24.0.0&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The big “Approve” button correctly needs a manager. But the plain “edit this report” form lets you type approved and name anyone as the approver, straight into the fields, with no manager required.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The attack in six steps&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expense reports need a second person to approve&lt;/strong&gt;: basic separation of duties.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dedicated Approve button checks for it&lt;/strong&gt;: the front door is genuinely locked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;But the generic "update report" route writes the same fields&lt;/strong&gt;: status and approver, set directly. &lt;em&gt;(this is the break)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing checks the approver differs from the author&lt;/strong&gt;: or that you hold the approval right at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mark your own claim approved and name anyone as approver&lt;/strong&gt;: from an account that can only file claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your claim, and a subordinate's, enter the payable queue&lt;/strong&gt;: the second signature never happened. &lt;strong&gt;→ self-approved payouts.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Expense approval is a textbook example of separation of duties: the person who files a claim must not be the person who approves it. That one rule is what stands between a company and an employee quietly signing off their own reimbursements. Dolibarr knows the rule, and the obvious button that approves a report enforces it properly.&lt;/p&gt;

&lt;p&gt;This is the story of a second, plainer path that writes the very same fields the Approve button does, without asking whether the person approving is the person who filed.&lt;/p&gt;

&lt;p&gt;The front door is locked. The side door was left open, and it leads to the same room.&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%2Ft0jjrjkyrtegvswb4gc7.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%2Ft0jjrjkyrtegvswb4gc7.png" alt=" " width="800" height="504"&gt;&lt;/a&gt;&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%2Fkbj6bc0zr8xlpbhgrrt4.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%2Fkbj6bc0zr8xlpbhgrrt4.png" alt=" " width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The question with an obvious answer, and a real one
&lt;/h2&gt;

&lt;p&gt;With any approval workflow, the first question to ask is the classic one: can the same person file a claim and approve it? We asked it of the dedicated Approve action, and the answer was a reassuring no. That action checks for the right permission, exactly as it should. A shallow security test stops here and calls it safe.&lt;/p&gt;

&lt;p&gt;The real answer lived one path over. Alongside the Approve button, Dolibarr has a generic “update this expense report” path, the kind of catch-all that lets you edit a report's details. And that path lets you write the report's &lt;em&gt;status&lt;/em&gt; and its &lt;em&gt;approver&lt;/em&gt; directly, as if they were ordinary fields like a date or a note.&lt;/p&gt;

&lt;p&gt;This is where API security testing and business logic testing matter. An endpoint can correctly protect its dedicated privileged action while another route exposes the same state change without enforcing the same authorization rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signing your own name on the approval line
&lt;/h2&gt;

&lt;p&gt;So an account that can only file claims opens the generic update path, sets the status to approved, and writes itself, or anyone it likes, into the approver field. Nothing checks that the approver differs from the author. Nothing checks that the account holds the approval right at all. The claim moves into the payable queue, approved, with a name on the approval line that the filer chose.&lt;/p&gt;

&lt;p&gt;We did it to our own claim, and then to a subordinate's, from an account that was never supposed to approve anything.&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%2Fuatqr4n1p6x8dl6unhmk.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%2Fuatqr4n1p6x8dl6unhmk.png" alt=" " width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a correct check isn't enough
&lt;/h2&gt;

&lt;p&gt;The uncomfortable lesson here is that the Approve button being correct bought almost nothing. A control is only as strong as the &lt;em&gt;weakest&lt;/em&gt; path that can reach the same state. If one carefully guarded action sets a field, and a second, generic action sets the same field with no guard, then the second action is the real security boundary, and it is wide open.&lt;/p&gt;

&lt;p&gt;The damage is money leaving on approvals that never happened. In a company relying on Dolibarr to enforce the two-person rule on reimbursements, this quietly dissolves it: a single employee can file and approve, or approve for others, and push claims into the queue that pays them.&lt;/p&gt;

&lt;p&gt;For Dolibarr security testing, this is an important distinction. Testing only the intended approval workflow can miss a business logic vulnerability exposed through a generic update endpoint. Effective penetration testing has to trace sensitive state changes across every route that can create them.&lt;/p&gt;

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

&lt;p&gt;The fix in &lt;strong&gt;24.0.0&lt;/strong&gt; brings the generic update path under the same rule the Approve action already enforced. The status and approver fields can no longer be written freely, and the separation between filing and approving holds on every path, not just the obvious one.&lt;/p&gt;

&lt;p&gt;The takeaway generalises to any workflow with a privileged step: find every path that can write the state, not just the one built for it. A locked front door means nothing while a back-office form writes the same fields with no check at all.&lt;/p&gt;

&lt;p&gt;This is also the kind of authorization gap that a web application penetration test should look for: not just whether a user can access an endpoint, but whether they can use an alternative endpoint to reach a privileged application state they were never meant to control.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The fix.&lt;/strong&gt; Upgrade to Dolibarr 24.0.0. That closes this finding. Want to know where your own app disagrees with itself? &lt;a href="https://codeant.ai/pentesting" rel="noopener noreferrer"&gt;→ Start with a free CodeAnt pentest&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/codeant/the-back-office-break-in-nine-dolibarr-findings-5cp8"&gt;← Back to all nine findings&lt;/a&gt;&lt;/p&gt;

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