Reporting & Professional Practice · beginner · ~10 min

Communication and working with developers

**What you will learn** - Translate a technical finding into plain-language business impact, calibrated to the audience (executive, product owner, or engineer). - State certainty honestly: distinguish "confirmed exploited" from "likely vulnerable, not confirmed," and never overstate. - Write a developer-facing finding that points to an exact location and offers a concrete, testable remediation. - Run good engagement hygiene: clarify scope early, escalate Critical findings immediately, and keep contemporaneous notes. - Handle the retest and hand-off so a fix is verified, not just promised.

Overview

Security objective. The asset you are protecting here is the fix itself — the change that actually reduces risk in the client's system — plus the trust relationship that lets a client keep working with you. The threat is not a hacker; it is a good finding that dies in the inbox: misunderstood by an executive, resented by a developer, or lost because scope was never clarified. This lesson teaches you to detect and prevent that failure so real findings become real fixes.

A penetration test produces value only when a human reads a finding, understands it, and changes the system. ("Penetration testing," or pentesting, means simulating real attacks — only against systems you are authorized to test — to find weaknesses before genuine attackers do.) The most technically brilliant exploit is worthless if the report is unclear, the tone is adversarial, or the urgent issue was buried on page 40.

This lesson builds directly on your prereq, "The penetration test report: structure." That lesson gave you the container — title, severity, affected component, reproduction, evidence, impact, remediation, retest. This lesson is about the human delivery of what goes in that container: how you talk about risk, how you work with the developers who own the code, and the professional habits that keep an engagement clean. It also draws on scope and ethics work from "pro-ethics-scope": everything below assumes you are operating inside an authorized, signed scope.

Three habits carry the whole lesson: translate risk into business impact, collaborate with developers instead of blaming them, and practice engagement hygiene (scope, escalation, notes).

Why it matters

In authorized professional work, communication is often what clients remember most. Two testers can find the same SQL injection; the one who explains it in terms the CTO understands, files it with a fix the developer can paste in, and re-tests it politely two weeks later is the one who gets re-hired.

Concretely, strong communication:

  • Gets findings fixed. A remediation the developer understands and trusts closes faster than a vague warning.
  • Protects you and the client. Contemporaneous notes and early scope clarification prevent "you touched a system you weren't allowed to" disputes.
  • Builds a security culture. When developers experience pentesters as allies, they start asking for reviews earlier — shifting security left.
  • Prevents dangerous delay. A Critical finding escalated the same day can stop an active breach; the same finding left for the final report can cost a company its data.

The throughline: penetration testing is a service to make systems safer. Findings are only the raw material.

Core concepts

1. Translate risk into business impact

Definition. Restating a technical finding in terms of the outcome a decision-maker cares about — money, data, reputation, downtime, compliance — rather than the mechanism.

Plain explanation. "CVE-2021-44228, unauthenticated RCE via JNDI lookup" means nothing to a finance director. "An attacker on the internet can run their own commands on our order server without logging in — that means read or delete every customer record" means everything. (A CVE is a standardized public identifier for a known vulnerability; RCE is remote code execution.)

How it works. Ask: If this is exploited, what does the business lose? Answer in one sentence, then add technical depth underneath for the engineers.

When / when not. Lead with business impact for executives and product owners. For the engineer who will fix it, keep the impact sentence but spend most of your words on the exact location and mechanism.

Pitfall. Over-dramatizing. If you call everything "catastrophic," nobody can tell your real Critical from a theoretical low. Severity depends on exploitability, required access, and impact — not on how scary the class of bug sounds.

2. Be honest about certainty

Definition. Clearly labeling how confident you are in each claim.

Plain explanation. There is a large difference between "I retrieved the admin password hash and cracked it" and "the login form reflects input unescaped, which may allow XSS — I did not confirm a working payload." Both are worth reporting; conflating them destroys trust.

How it works. Use explicit language: confirmed, demonstrated, likely, suspected, theoretical. Tie the label to your evidence.

When / when not. Always. Especially resist the pressure to inflate certainty to make a report look more impressive.

Pitfall. "Decoding a JWT proves it's forgeable." No. Decoding a JWT just reads its Base64 payload; it says nothing about whether the signature verifies. Claiming a forge you did not perform is a false certainty that will embarrass you at retest.

3. Collaborate with developers, not against them

Definition. Framing every finding as a shared problem to solve, with the developer, aimed at a safer product.

Plain explanation. The developer did not "fail" — they shipped under a deadline with constraints you may not see. Your job is to hand them a specific, fixable problem, not a verdict on their competence.

How it works. For each finding give: exact location (file/line, endpoint, parameter), the mechanism, a concrete remediation, and an offer to clarify or re-test. Suggest a pragmatic interim mitigation when a full fix is slow.

When / when not. Collaborative tone always. The one exception to "work it out together" is an active, in-progress compromise — that you escalate immediately, not negotiate.

Pitfall. Public blame. Never dunk on a team in a shared channel or leadership deck. It poisons the relationship and future engagements.

4. Engagement hygiene

Definition. The operational discipline around a test: scope clarification, escalation, and note-taking.

Plain explanation. Ambiguity is where engagements go wrong. If it is unclear whether a subdomain is in scope, you ask — you never assume yes. Critical findings get raised the moment you confirm them. And you write down what you did, when, and against what, as you go.

How it works. Keep a running log (timestamp, target, action, result). Have an agreed escalation contact and channel before the test starts. Send short status updates covering progress, blockers, and any urgent finding.

When / when not. Always on an authorized engagement. Notes are never optional; they become the report and they protect you if a client asks "did you cause this outage?"

Pitfall. Waiting for the final report to mention a Critical. That delay can be the difference between a contained issue and a breach.

THREAT MODEL: the finding-to-fix pipeline

  [Pentester]                trust boundary            [Client org]
  authorized scope   ─────────────┊──────────────►   fixes the system
                                   ┊
  ASSETS TO PROTECT                ┊   ENTRY POINTS FOR FAILURE
  - the fix (real risk         ┌───┊───┐   • ambiguous scope -> tested wrong host
    reduction)                 │ report │   • jargon -> exec ignores it
  - client trust               │  +     │   • blame -> dev resists fix
  - engagement evidence        │ updates│   • buried Critical -> breach window
                               └───┊───┘   • overstated certainty -> failed retest
                                   ┊
  DETECTION (what catches failure) ┊
  - contemporaneous notes / activity log
  - status updates + escalation channel
  - retest result (fix verified vs not)

Knowledge check.

  1. What asset is this lesson protecting — the exploit, or the fix and the client relationship?
  2. Where is the trust boundary, and which entry point turns a great finding into a wasted one?
  3. Which insecure assumption causes "I tested a host that wasn't in scope," and what habit prevents it?
  4. Which record lets you prove, weeks later, exactly what you did and when?
  5. Why must every action in your notes have happened only inside an authorized scope?

Syntax notes

The "syntax" of this lesson is the shape of the artifacts you produce: a developer-facing finding and a status update. Keep them short and structured.

FINDING (developer-facing)
--------------------------------
Title:        <specific, no hype>   e.g. "Stored XSS in profile 'bio' field"
Severity:     <Critical|High|Med|Low|Info>  (justify by exploitability + impact)
Certainty:    <Confirmed | Likely | Suspected | Theoretical>
Location:     <endpoint / file:line / parameter>
Mechanism:    <one or two sentences: what the code does wrong>
Business impact: <one plain sentence a non-engineer understands>
Remediation:  <concrete, testable fix>
Retest:       <how you will verify the fix>
STATUS UPDATE (to client point of contact)
--------------------------------
Progress:  what was tested since last update
Blockers:  anything stopping you (access, scope question)
Urgent:    any Critical/High that cannot wait for the final report
Next:      what you test next

Notice the two things that appear in every finding but are easy to skip: an explicit certainty label and a business impact line written for a human, not a scanner.

Lesson

The final professional skill is turning findings into fixes through people. Even the best report fails if it is unclear or adversarial.

Communicate risk simply

  • Translate technical findings into business impact for non-technical stakeholders. Say "this lets anyone read customer data" rather than quoting a CVE string. (A CVE is a standardized identifier for a known vulnerability.)
  • Calibrate to the audience: technical depth for engineers, outcomes for executives.
  • Be precise about uncertainty. Distinguish "confirmed exploited" from "likely vulnerable, not confirmed."

Work with developers, not against them

  • Stay collaborative, not adversarial. The goal is a more secure product, shared with the dev team. It is not to prove they failed. Tone matters.
  • Be specific and actionable. Point to the exact code or endpoint and the concrete fix. Offer to clarify and re-test.
  • Respect constraints. Developers have deadlines and trade-offs. Help them prioritize, and suggest pragmatic mitigations when a full fix will take time.

Engagement hygiene

  • Ask for scope clarification early whenever anything is ambiguous. Do not assume.
  • Write clear client updates. Cover status, blockers, and any urgent findings. Surface a Critical finding immediately rather than waiting for the final report.
  • Take good notes throughout. They become the report, and they protect you.

The throughline

Penetration testing is ultimately a service to make systems safer. Technical findings create value only when they are communicated clearly, scoped honestly, and handed to the people who can fix them, in a constructive way.

Code examples

The deliverables here are written communication, not compiled code. Below is the same finding written poorly, then well, then a verification you would run at retest. All content refers to a local, authorized lab app only.

WARNING: intentionally vulnerable — use only in a local, isolated, authorized lab. Do not deploy.

(1) The insecure app + the WRONG way to report it

# lab_app.py  -- runs on http://localhost:5000 in an isolated lab ONLY
# Vulnerable: user 'bio' is stored and rendered without escaping (stored XSS).
from flask import Flask, request, render_template_string
app = Flask(__name__)
BIOS = {}

@app.route("/save", methods=["POST"])
def save():
    BIOS["me"] = request.form["bio"]        # stored as-is
    return "saved"

@app.route("/profile")
def profile():
    # BUG: raw interpolation -> script in bio executes in the viewer's browser
    return render_template_string("<h1>Bio</h1><div>" + BIOS.get("me", "") + "</div>")

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000)

A WRONG report reads: "XSS on profile page. Critical!!! Devs didn't sanitize anything, classic mistake." — vague location, hype severity, blame, no fix, no certainty.

(2) The developer-facing finding done well (the SECURE hand-off)

Title:        Stored XSS in profile 'bio' field
Severity:     High  (authenticated user can persist script that runs for
              every viewer of the profile; no admin access required)
Certainty:    Confirmed  (demonstrated alert() firing in a lab browser)
Location:     GET /profile , value stored via POST /save param 'bio'
              lab_app.py, profile(): string-concatenated into HTML
Mechanism:    'bio' is concatenated into the response HTML without output
              encoding, so <script> in the value executes in the viewer.
Business impact: A logged-in user can plant code that runs in other users'
              browsers when they view the profile -- e.g. steal their session.
Remediation:  Stop concatenating. Autoescape by rendering through a template
              that HTML-encodes variables (Jinja autoescape ON), or encode
              the value on output. Treat 'bio' as data, never markup.
Retest:       Re-submit the test payload; confirm it renders as inert text.

The fixed code:

# profile() fixed: autoescaping template treats bio as text, not HTML.
from markupsafe import escape

@app.route("/profile")
def profile():
    bio = BIOS.get("me", "")
    # escape() turns < > & " into HTML entities, so scripts cannot execute.
    return "<h1>Bio</h1><div>" + str(escape(bio)) + "</div>"

(3) VERIFY — a check that the fix REJECTS bad input and ACCEPTS good input

# retest.py -- run against the LOCAL lab app only.
import requests
BASE = "http://127.0.0.1:5000"

def put(bio): requests.post(BASE + "/save", data={"bio": bio})
def page():  return requests.get(BASE + "/profile").text

# Bad input: a script payload must NOT appear as a live <script> tag.
put("<script>alert(1)</script>")
html = page()
assert "<script>alert(1)</script>" not in html, "FAIL: payload still live (vulnerable)"
assert "&lt;script&gt;" in html, "FAIL: payload not encoded"

# Good input: ordinary text must still render normally.
put("Hello, I love math.")
assert "Hello, I love math." in page(), "FAIL: legit text rejected"

print("PASS: malicious input neutralized, legitimate input preserved")

Expected result. Against the vulnerable version, retest.py fails the first assertion (the raw <script> is still present). Against the fixed version it prints PASS, proving the payload is encoded to &lt;script&gt; (inert text) while normal text is untouched. That PASS line is what you attach as retest evidence — concrete proof, not a promise.

Line by line

Walking the fixed profile() and the retest, since that is where the mitigation is proven.

Step Line What happens Why it matters
1 bio = BIOS.get("me", "") Fetch the stored value, which may contain attacker input The value is untrusted data, not trusted markup
2 escape(bio) <, >, &, " become &lt;, &gt;, &amp;, &quot; The browser now renders them as visible characters, not tags — the script cannot execute
3 return concatenated HTML The encoded string is placed in the page Output encoding at the point of use is the correct XSS defense

Retest trace:

Step Action Vulnerable app Fixed app
A put("<script>alert(1)</script>") stored raw stored raw (storage is fine)
B page() returns HTML contains live <script>alert(1)</script> contains &lt;script&gt;alert(1)&lt;/script&gt;
C assert payload not live fails (still vulnerable) passes
D assert encoded form present fails passes
E good text "Hello, I love math." present present (legit input preserved)

The key insight: the fix must be verified by both directions — malicious input is neutralized (C, D) and legitimate input still works (E). A fix that also breaks normal bios is not a real fix, and the retest is how you catch that before you write "Resolved."

Common mistakes

Mistake 1 — Leading with jargon to an executive.

  • WRONG: "We found CVE-class reflected and stored XSS plus an IDOR."
  • WHY WRONG: The decision-maker cannot weigh risk they cannot understand, so nothing gets funded.
  • CORRECTED: "A logged-in user can plant code that runs in other customers' browsers and read their session — that's account takeover. Details for engineering are in section 4."
  • RECOGNISE/PREVENT: If a non-engineer can't restate the impact back to you, rewrite it.

Mistake 2 — Overstating certainty.

  • WRONG: Reporting "authentication bypass" because you decoded a JWT and saw admin:true.
  • WHY WRONG: Decoding is not verifying the signature. If the server checks the signature, your 'bypass' fails at retest and your credibility drops.
  • CORRECTED: "The JWT payload contains a role claim (readable by design). I did NOT confirm the signature can be forged — marked Suspected pending a signature-verification test."
  • RECOGNISE/PREVENT: Attach a certainty label to every claim and make sure your evidence supports it.

Mistake 3 — Blaming the team.

  • WRONG: "The developers clearly don't understand security basics."
  • WHY WRONG: It makes the fix a fight, and it's unprofessional in a shared document.
  • CORRECTED: "Here's the exact endpoint, the mechanism, and a fix you can drop in. Happy to pair on the retest."
  • RECOGNISE/PREVENT: Write every finding as "here is a problem and a fix," never "here is who failed."

Mistake 4 — Sitting on a Critical.

  • WRONG: Finding an unauthenticated database dump on day 1 and saving it for the day-10 report.
  • WHY WRONG: The exposure — possibly already being exploited — stays open for nine extra days.
  • CORRECTED: Escalate the same day via the agreed contact/channel, in writing, then continue testing.
  • RECOGNISE/PREVENT: Agree an escalation path before the test starts; define what severity triggers it.

Mistake 5 — No contemporaneous notes.

  • WRONG: Reconstructing what you did from memory after an outage the client blames on you.
  • WHY WRONG: Without a timestamped log you cannot show your actions were in scope and benign.
  • CORRECTED: Keep a running log (time, target, action, result) throughout.
  • RECOGNISE/PREVENT: Start the log before the first request, not after the first problem.

Debugging tips

When communication fails, treat it like a bug and isolate the cause.

  • Symptom: the finding was ignored. Ask: did the reader understand the impact or just the mechanism? Rewrite the top line as one plain-English business sentence and resend.
  • Symptom: the developer pushed back hard. Ask: was the tone a verdict or a hand-off? Did you give an exact location and a concrete fix? Re-send as "problem + fix + offer to pair," drop any blame language.
  • Symptom: retest failed / the 'fix' didn't hold. Ask: was your original certainty honest? Re-run your verification (like retest.py) and confirm the fix both blocks bad input and preserves good input before reopening or closing.
  • Symptom: a scope dispute ("you hit a system you shouldn't have"). Ask: is it in your notes and the signed scope? Produce the timestamped log; if scope was ambiguous, that's the lesson — clarify in writing next time before touching anything.
  • Symptom: the client says a Critical was 'buried.' Ask: did you escalate out-of-band the day you confirmed it? Check your status-update trail. If it's only in the final PDF, that's the process gap to fix.

Questions to ask when delivery fails: Who was the audience, and did I match the detail to them? Was my certainty label truthful? Was there an exact location and a testable fix? Did the urgent stuff travel faster than the report?

Memory safety

Security & safety: detection, logging, and evidence.

Communication artifacts are evidence, and evidence has to be handled safely.

What to log (your activity record and the client's detection):

  • Timestamp (with timezone) of each action.
  • Source — which host/account you tested from.
  • Resource — the exact endpoint, parameter, or host targeted.
  • Result — what happened (e.g., "200, reflected input executed").
  • Security decision — was it in scope; did you stop on a Critical.
  • A correlation id per finding so notes, report, and retest line up.

On the client side, the events that signal abuse (and that a good report tells the team to alert on): bursts of failed logins, input containing script/SQL metacharacters, access to another user's object id (IDOR probing), and requests to out-of-scope or admin paths. A finding is far more useful when you tell the team which log line would have caught this.

What to NEVER put in a report, screenshot, or note: real passwords or password hashes you cracked, session cookies or tokens, private keys, full payment card numbers (mask them), and any PII beyond the minimum needed to prove impact. If you must show a credential worked, redact it (Pa$$w0rd -> Pa******) and store any raw proof encrypted, shared only over an agreed secure channel.

False positives arise the same way here as in detection: a scanner flags a 'vulnerability' that isn't exploitable, or a WAF blocks a benign payload and looks like an attack. This is exactly why the certainty label matters — mark unconfirmed items as Suspected, and verify before you escalate, so you don't burn the team's trust with noise.

Real-world uses

Authorized real-world use case. A firm hires you for a two-week web app test under a signed scope. On day 2 you confirm an unauthenticated data-export endpoint. You (a) escalate the same afternoon via the agreed channel with a masked proof and a one-line business impact; (b) file the developer finding with exact endpoint, mechanism, and a concrete auth-check fix; (c) keep it out of the exec deck as blame and instead present "here's the risk, here's the fix, here's the retest plan." Two weeks later you re-run your verification, attach the PASS output, and mark it Resolved. That loop — escalate, hand off, retest — is the job.

Best-practice habits:

Habit Beginner Advanced
Validation of your own claims Label each finding Confirmed/Suspected Attach reproducible proof + retest script per finding
Least privilege Only touch in-scope hosts Use least-access test accounts; document any privilege used
Secure defaults in advice Recommend output encoding, parameterized queries Recommend defense-in-depth (encode + CSP + input validation)
Logging Keep a timestamped activity log Give the client the exact detection rule/alert to add
Error handling / tone Problem + fix, never blame Pragmatic interim mitigation + full fix + prioritization help

Correct the common misconceptions when you present: decoding a JWT is not verifying its signature; passing an automated scanner does not prove a system is secure (scanners miss logic flaws and produce false positives); and never claim a system is "completely secure" — you report residual risk, not a clean bill of health.

Practice tasks

All tasks are lab-only, against software you run locally or are explicitly authorized to test. Each ends by remediating and verifying — defense, not exploitation.

Beginner 1 — Translate a finding.

  • Objective: Rewrite a jargon finding for a non-technical audience.
  • Input: "Reflected XSS via unescaped q parameter on /search."
  • Output: One plain-English business-impact sentence + a one-line technical note for engineers.
  • Constraints: No CVE strings in the business sentence; no hype words.
  • Hints: Answer "what does the business lose if this is exploited?" first.
  • Concepts: risk translation, audience calibration.

Beginner 2 — Certainty labels.

  • Objective: Classify five claims as Confirmed / Likely / Suspected / Theoretical.
  • Input: Include the JWT case: "I decoded the token and saw role=admin."
  • Output: A label per claim with a one-line justification.
  • Constraints: The JWT claim must not be labeled Confirmed bypass.
  • Hints: Ask what evidence would move each up a level.
  • Concepts: honest certainty, evidence-to-claim matching.

Intermediate 1 — Developer-facing finding.

  • Objective: Write a full finding for the lab XSS from this lesson.
  • Requirements: Title, severity (justified by exploitability+impact), certainty, exact location, mechanism, business impact, concrete remediation, retest plan.
  • Constraints: Collaborative tone; no blame; remediation must be testable.
  • Hints: Reuse the FINDING template in syntax_notes.
  • Concepts: collaboration, actionable remediation.
  • Defensive conclusion: apply the encoding fix in your lab, then run a retest that shows the payload is inert.

Intermediate 2 — Status update + escalation.

  • Objective: Draft a mid-engagement status update where day-2 testing uncovers an unauthenticated data-export endpoint in your lab app.
  • Requirements: Progress / Blockers / Urgent / Next sections; the Urgent item escalated out-of-band with masked proof.
  • Constraints: No real secrets; redact any sample credential; localhost only.
  • Hints: Urgent must not wait for the final report.
  • Concepts: engagement hygiene, escalation, safe evidence handling.
  • Defensive conclusion: state the interim mitigation (require auth) and how you'll retest it.

Challenge — Full finding-to-fix loop with an authorization checklist.

  • Objective: Run one vulnerability end-to-end in an isolated lab (the provided Flask app or a deliberately-vulnerable VM you own).
  • Requirements: (1) an authorization checklist you complete before starting — I own or am authorized to test this host; target is localhost/container/CTF; scope written down; escalation contact known; cleanup plan ready. (2) contemporaneous activity log (timestamp, target, action, result, correlation id). (3) developer finding. (4) apply fix. (5) verification script proving bad input rejected and good input accepted. (6) a retest note marking it Resolved with the PASS evidence.
  • Constraints: No real targets, no working credential-stealers or persistence; masked proof only; never claim "completely secure."
  • Hints: Model the loop on the code + retest.py in this lesson.
  • Concepts: everything above, integrated.
  • Cleanup / reset: stop the lab server, wipe the stored bio/test data (BIOS.clear() or restart the process), delete any captured proof files from the lab host, and revert the app to a clean state so the next run starts fresh.

Authorization checklist (use whenever a lab is involved): written permission or you own the system; target is localhost / a container / an intentionally-vulnerable VM / a sanctioned CTF; scope boundaries written down; an escalation contact and channel agreed; a cleanup/reset plan ready before you start.

Summary

  • Findings only matter when they become fixes. The asset you protect is the fix and the client relationship — the threat is a good finding that dies in the inbox.
  • Translate risk into business impact, calibrated to the audience: one plain sentence for leaders, exact location and mechanism for engineers.
  • Be honest about certainty — Confirmed vs Suspected vs Theoretical. Decoding a JWT is not verifying its signature; a passed scanner does not prove security; nothing is "completely secure."
  • Collaborate, never blame. Every finding = exact location + mechanism + concrete, testable fix + an offer to retest.
  • Engagement hygiene: clarify scope early (never assume), escalate Critical findings out-of-band the day you confirm them, keep contemporaneous notes (timestamp, target, action, result, correlation id).
  • Verify the fix both ways: malicious input rejected AND legitimate input preserved; attach the PASS evidence at retest.
  • Log the right things, never log secrets (passwords, tokens, keys, full PANs, extra PII); tell the client which log line detects the abuse. Everything happens inside authorized scope.

Practice with these exercises