Reporting & Professional Practice · beginner · ~10 min
**What you will learn** - Explain the four non-negotiable duties of authorized testing: authorization, scope, do-no-harm, and data protection — and why "reachable" never means "authorized." - Recognize concrete **stop conditions** (a prior real breach, a production-safety risk, drifting out of scope) and follow the correct escalation path immediately. - Handle sensitive data ethically: minimize access, avoid exfiltration, and store or destroy evidence per the engagement rules. - Apply **coordinated (responsible) disclosure** for issues you find outside a formal engagement, such as in open-source software. - Keep a defensible **decision log** that proves you acted lawfully and within scope, and know which events to record and which secrets to never write down. - Correct the common misconception that being technically able to do something makes it legal or permitted.
Security objective: the asset you are protecting here is trust and legality — the client's data and systems, the public who depends on them, and your own legal standing as a tester. The threat is you: an authorized tester who, through carelessness or ambition, causes harm, breaks the law, or destroys the client's trust. By the end of this lesson you will be able to detect the moments where a test crosses an ethical or legal line and stop before damage is done.
This lesson builds directly on your prereq "Authorization, scope, and rules of engagement" (methodology-rules-of-engagement). There you learned what a rules-of-engagement (RoE) document contains — the signed authorization, the in-scope target list, the time window, and the emergency contacts. Here you learn how to live inside those boundaries under pressure: what to do when a genuinely interesting finding sits just outside scope, when you stumble onto evidence that a real attacker got there first, or when a test is about to knock production offline.
Ethics is not a soft add-on to technical skill — it is the thing that makes the skill legitimate. The exact same port scan, password test, or web request is professional security work when it is authorized and in scope, and a computer crime when it is not. The only difference is permission and restraint. Everything in this lesson is about staying on the right side of that line, and being able to prove you did.
In real, authorized work, ethics and legality are the foundation the entire profession stands on. A penetration test is a controlled attack a client invites into their systems because they trust you to stop before you cause harm. That trust is the product. Cross a line and three things happen at once: you can commit a crime under computer-misuse laws (many carry criminal penalties), you can damage the client (deleted data, downed production, leaked customer records), and you can end your own career — one out-of-scope incident follows a tester for a long time.
Clients, insurers, and courts do not take your word that you behaved. They look at the signed authorization, the scope definition, and your logs. A tester who can produce a clean decision trail — "here is the authorization, here is where I stopped, here is who I called and when" — is defensible. A tester who cannot is exposed, even if they meant well.
Knowing when to stop is a senior skill, not a beginner limitation. Junior testers chase every finding to full exploitation; professionals recognize when a proof-of-concept already proves the point, when a finding needs immediate escalation, and when continuing adds risk without adding value. That judgment is what clients actually pay for.
Each duty below is a separate skill. Learn the definition, the plain idea, how it works in practice, when it applies (and when it does not), and the pitfall that trips up beginners.
Definition: explicit, written permission from someone with the authority to grant it, for specific systems, over a specific time window.
Plain explanation: authorization is the piece of paper that turns "attack" into "test." Without it, everything you do is potentially a crime, no matter how good your intentions.
How it works: the RoE or engagement contract names the authorizing party, the targets, the dates/times, and the permitted techniques. You keep a copy accessible during the test (a "get-out-of-jail" letter) so you can prove authorization on the spot.
When / when not: you act only when covered by current, signed authorization. If the window has expired, or the person who "said it was fine" verbally has no authority to authorize, you do not have authorization.
Pitfall: treating a verbal "sure, go ahead" as authorization. Verbal permission from the wrong person is worthless in court and unfair to the client.
Definition: the exact set of systems, addresses, applications, accounts, and techniques you are allowed to touch.
Plain explanation: the network will happily let you reach machines you were never hired to test — a shared cloud tenant, a third-party API, a neighbouring company on the same subnet. Being able to reach a system is a networking fact; being allowed to test it is a legal fact. They are unrelated.
How it works: scope is defined by an allow-list (IP ranges, domains, apps). Anything not on the list is out of scope by default. When in doubt, it is out.
When / when not: stay strictly inside the allow-list. If a valuable finding leads toward an out-of-scope system (a very common situation), you stop at the boundary and ask before crossing.
Pitfall: "scope creep" — following an attack chain across a trust boundary because it is technically possible and exciting. That is exactly how testers end up attacking a third party who never consented.
Definition: prefer the least-damaging action that still demonstrates the issue; get explicit sign-off before anything risky.
Plain explanation: you do not need to fully exploit a bug to prove it exists. A proof-of-concept (PoC) — a screenshot, a single benign marker file, a limited read — usually proves the point without breaking anything.
How it works: destructive and disruptive tests (denial-of-service, mass deletion, ransomware-style actions, uploading real malware) are forbidden unless the contract specifically authorizes them, and even then only in agreed windows. You choose the gentlest evidence that a reviewer would accept.
When / when not: demonstrate impact, do not cause it. If proving a finding requires real damage, you stop and describe the impact instead, or negotiate a controlled test.
Pitfall: "just one more step" to make the finding look dramatic. Dumping an entire production database to prove SQL injection is harm, not proof — a single row or a SELECT version() proves it.
Definition: minimize what real sensitive data you access, never exfiltrate it, and secure or destroy any evidence.
Plain explanation: during a test you may encounter real customer records, passwords, or secrets. You are a temporary, trusted guest. Look only as much as needed to prove the finding, and never copy sensitive data out of the environment.
How it works: Exfiltrate means copying data out of the target to somewhere you control. As a tester you do not do this. Evidence (screenshots, request logs) is stored encrypted, shared over agreed secure channels, and deleted on the schedule in the contract.
When / when not: access the minimum needed to demonstrate the issue. Never take copies home; never post evidence in a public issue tracker or chat.
Pitfall: screenshotting a full table of real Social Security numbers when a single redacted record would have proved the same thing.
Definition: predefined situations where you halt testing and escalate immediately.
Plain explanation: some discoveries are more important than finishing your task. The clearest is finding that a real attacker was already here — signs of a prior breach. That is now an incident, not a test finding.
How it works: on a stop condition you (1) cease the relevant activity, (2) preserve what you have without altering the scene, and (3) contact your designated point of contact using the agreed emergency channel — now, not in the final report.
Common stop conditions: evidence of a prior/ongoing real breach; a critical safety-impacting issue (e.g. medical, industrial, or life-safety systems); an unplanned production outage or instability you caused or risk causing; realizing you have drifted out of scope.
Pitfall: treating a live-breach discovery as "a cool finding for the report" and continuing to poke — which can tip off the attacker, destroy forensic evidence, or make you look complicit.
Definition: the ethical process for reporting a vulnerability you found outside a paid engagement — for example in open-source software or a public product.
Plain explanation: finding a bug in software you use does not authorize you to attack anyone running it. Disclosure is how you get the issue fixed without harming users.
How it works: report privately to the owner (a security contact, security.txt, or a bug-bounty program), give reasonable time to fix, coordinate on timing, and do not publish working exploits against systems that are still unpatched.
When / when not: use it for issues found in your own lab, in OSS, or in products with a disclosure program. It does not grant permission to test live third-party systems — reading source code or testing your own local copy is fine; hammering someone's production server is not.
Pitfall: "full disclosure" dumps — publishing a working exploit before a patch exists — which endangers every user of the software.
THREAT MODEL: the authorized test itself as the risk
ASSETS PROTECTED
- Client data (customer records, secrets, PII)
- Client availability (production must keep running)
- Public / third parties on shared infrastructure
- Legality + the tester's own standing
ENTRY POINTS (where harm gets introduced)
- Tester's tools running against targets
- A tempting attack chain leading off-scope
- Sensitive data encountered mid-test
- A discovered real prior breach
IN SCOPE (authorized) | OUT OF SCOPE (forbidden)
+-----------------------------+ | +---------------------------+
| Listed IPs / domains /apps | | | Neighboring tenant |
| Agreed time window | == | ==| Third-party API / SaaS |
| Agreed techniques | | | Anything not on allow-list|
+-----------------------------+ | +---------------------------+
TRUST BOUNDARY
(reachable != authorized; do not cross
without new written permission)
CONTROLS
- Signed authorization kept on hand
- Allow-list checked before every new target
- PoC-not-full-exploit rule
- Stop-and-escalate on breach/safety/outage
- Timestamped decision log
Knowledge check
This is a professional-practice lesson, so the "syntax" is the structure of an authorization check and a decision-log entry — the artifacts that keep you defensible. Below is a lab-safe template you can adapt.
BEFORE-YOU-TOUCH CHECK (run mentally + in your notes for every new target)
[ ] Signed authorization exists and is current (date within window)
[ ] Target is on the in-scope allow-list (exact IP/domain/app)
[ ] Technique is permitted (no DoS/destructive unless signed off)
[ ] I know the emergency contact + channel for stop conditions
-> If ANY box is unchecked: DO NOT PROCEED. Ask first.
DECISION-LOG ENTRY (append-only; one line per meaningful action/decision)
2026-07-07T14:03:11Z | src=10.0.0.5(lab) | target=10.0.0.20:8080
action="read one row to confirm SQLi" | scope=IN | result=confirmed
decision="PoC sufficient, did not dump table" | corr_id=ENG-42-017
Key points in the annotation:
...Z) so events line up with the client's own logs.scope=IN/OUT forces you to make the boundary decision explicit.corr_id (correlation id) ties this action to the engagement and to any alert the client's monitoring raises.Technical skill without ethics is a liability. A professional tester works inside firm legal and ethical lines. That is what separates the job from a crime.
Some issues turn up outside an engagement, for example in open-source software. For those, follow responsible (coordinated) disclosure:
Because ethics is about decisions, the "code" here is a small, self-contained Python helper you can run in your own lab to enforce the scope and authorization rules before any action. It shows the INSECURE → SECURE → VERIFY shape: first the naive check that lets scope creep through, then the corrected gate, then tests proving it rejects out-of-scope/expired requests and accepts valid ones. No network calls are made — it is pure decision logic.
# WARNING: intentionally vulnerable — use only in a local, isolated, authorized
# lab. Do not deploy.
#
# INSECURE: "can I reach it?" mistaken for "am I allowed?"
import socket
def may_test_insecure(host, port):
# BUG: reachability is treated as permission. This is the exact
# "reachable == authorized" fallacy. It ignores scope AND the clock.
try:
with socket.create_connection((host, port), timeout=1):
return True # WRONG: being able to connect proves nothing
except OSError:
return False
# SECURE: authorization + scope + time window are checked explicitly.
# Reachability is irrelevant to permission.
from datetime import datetime, timezone
from ipaddress import ip_address, ip_network
# In real work these come from the signed RoE, not hardcoded.
AUTH_SIGNED = True
SCOPE_ALLOWLIST = [ip_network("10.0.0.0/24")] # lab range only
WINDOW_START = datetime(2026, 7, 7, 9, 0, tzinfo=timezone.utc)
WINDOW_END = datetime(2026, 7, 7, 18, 0, tzinfo=timezone.utc)
DESTRUCTIVE_ALLOWED = False
def may_test(host, *, destructive=False, now=None):
"""Return (allowed: bool, reason: str). Fails closed."""
now = now or datetime.now(timezone.utc)
if not AUTH_SIGNED:
return (False, "no signed authorization")
if not (WINDOW_START <= now <= WINDOW_END):
return (False, "outside authorized time window")
try:
ip = ip_address(host)
except ValueError:
return (False, "target is not a literal IP in the allow-list")
if not any(ip in net for net in SCOPE_ALLOWLIST):
return (False, "target is OUT OF SCOPE (reachable != authorized)")
if destructive and not DESTRUCTIVE_ALLOWED:
return (False, "destructive technique not signed off")
return (True, "authorized, in scope, in window")
# VERIFY: prove the gate REJECTS bad requests and ACCEPTS good ones.
from datetime import datetime, timezone
IN_WINDOW = datetime(2026, 7, 7, 12, 0, tzinfo=timezone.utc)
LATE = datetime(2026, 7, 7, 23, 0, tzinfo=timezone.utc)
def check(label, got, expect):
ok = (got[0] == expect)
print(f"[{'PASS' if ok else 'FAIL'}] {label}: {got[1]}")
assert ok, got
# REJECT cases
check("out-of-scope host", may_test("8.8.8.8", now=IN_WINDOW), False)
check("outside time window", may_test("10.0.0.20", now=LATE), False)
check("destructive w/o signoff",
may_test("10.0.0.20", destructive=True, now=IN_WINDOW), False)
# ACCEPT case
check("in scope + in window", may_test("10.0.0.20", now=IN_WINDOW), True)
print("all checks complete")
Expected output (order preserved; every line PASS):
[PASS] out-of-scope host: target is OUT OF SCOPE (reachable != authorized)
[PASS] outside time window: outside authorized time window
[PASS] destructive w/o signoff: destructive technique not signed off
[PASS] in scope + in window: authorized, in scope, in window
all checks complete
What this teaches: the insecure version answers "can I connect?" — a networking question. The secure may_test answers "am I permitted?" — a legal question — and fails closed (defaults to "no") on anything unexpected, including a hostname it cannot resolve to an allow-listed IP. The verify block is the mitigation-verification step: it demonstrates the gate blocks the three ways testers go wrong (off-scope, off-clock, unauthorized destructive) while still allowing legitimate work.
Walkthrough of the SECURE may_test gate and its verification.
now = now or datetime.now(timezone.utc) — the decision is time-aware. Authorization is not permanent; it expires at the window's end. Using UTC (timezone.utc) means your timestamps line up with the client's logs later.if not AUTH_SIGNED: — the very first gate. No signed authorization ⇒ immediate False. Everything else is irrelevant without this.if not (WINDOW_START <= now <= WINDOW_END): — even with authorization, acting outside the agreed window is unauthorized. This catches the "I'll just finish tonight" mistake.ip = ip_address(host) inside try/except — if host is not a literal IP we can place on the allow-list, we refuse rather than guess. This fails closed: ambiguity becomes "no."if not any(ip in net for net in SCOPE_ALLOWLIST): — the core scope check. The target must fall inside an allow-listed network. 8.8.8.8 is reachable from almost anywhere, yet it is not in 10.0.0.0/24, so it is rejected — the code encodes "reachable ≠ authorized."if destructive and not DESTRUCTIVE_ALLOWED: — destructive techniques need their own explicit sign-off, checked separately from ordinary in-scope permission.return (True, ...) — only if every gate passed. The function returns a reason string in all cases so the decision can be written to the log.Trace of the verify block:
| Call | AUTH |
window? | in allow-list? | destructive ok? | Result |
|---|---|---|---|---|---|
8.8.8.8, in-window |
yes | yes | no | n/a | False — out of scope |
10.0.0.20, LATE (23:00) |
yes | no | — | — | False — outside window |
10.0.0.20, destructive |
yes | yes | yes | no | False — no sign-off |
10.0.0.20, in-window |
yes | yes | yes | n/a | True — permitted |
Notice the gate short-circuits at the first failing condition, so the reason string always names the specific rule that stopped you — exactly what you want in a decision log.
Real mistakes testers make, why they are wrong, the correction, and how to catch them.
Why wrong: reachability is a networking accident, not permission. Testing an out-of-scope host can be a crime and can hit an innocent third party on shared infrastructure.
Corrected: check the allow-list before every new target; stop at the trust boundary and request written permission before crossing.
Recognize/prevent: if you cannot point to the target on the scope document, you are out of scope. Build the allow-list check into your workflow (see the may_test gate).
Why wrong: continuing to poke can alert the attacker, destroy forensic evidence, or make it look like you caused the incident. It also delays the client's response to a live threat. Corrected: stop the relevant activity, preserve evidence without altering it, and escalate to your point of contact immediately via the agreed channel. Recognize/prevent: ask "do these timestamps/actions predate my window and not match my source?" If yes, it is not you — escalate now.
Why wrong: a full production database dump is harm and a data-protection violation, not evidence. It also creates a copy of sensitive data you now have to protect.
Corrected: capture the minimum PoC a reviewer would accept — one redacted record, a SELECT version(), a single benign marker.
Recognize/prevent: before running an action, ask "what is the smallest thing that proves this?" If the answer is smaller than what you were about to do, do the smaller thing.
Why wrong: verbal permission from someone without authority is unenforceable and unfair to the client. It leaves you undefended if anything goes wrong. Corrected: require signed authorization from someone empowered to grant it, and keep a copy on hand during the test. Recognize/prevent: no signature and no named authorizer on the document ⇒ do not start.
Why wrong: these overclaims mislead the client. Decoding a token (e.g. base64-decoding a JWT) reveals its contents but does not verify its signature or prove it is trusted. A clean automated-scanner run means the scanner found nothing — not that the system is secure. Corrected: state findings precisely: "decoded, not signature-verified"; "scanner reported no issues, manual testing still required." Never claim a system is "completely secure" — say "no issues found within this scope and time." Recognize/prevent: reread every claim and ask "does my evidence actually support this exact sentence?"
When an ethical decision "fails," the symptom is usually uncertainty or an unexpected escalation. Debug it deliberately.
Common failure: "Is this target in scope?" is unclear.
Common failure: the may_test gate rejects something you expected to pass.
now actually inside WINDOW_START..WINDOW_END? Time-zone/UTC mistakes are the usual cause.Common failure: you think you may have caused an outage.
Common failure: you are not sure whether a finding is a prior real breach.
General debugging question set for any ethical doubt:
Security & safety — detection, logging, and evidence handling
Ethical testing is only defensible if it is recorded. Your logs are what prove you stayed lawful and in scope, and the client's logs are how they distinguish your authorized activity from a real attacker.
What to log (for every meaningful action/decision):
2026-07-07T14:03:11Z) so it aligns with the client's monitoring.What to NEVER log or store in the clear:
API_KEY=<development-placeholder>).Which events signal abuse (and should be escalated):
How false positives arise: your legitimate testing will trip the client's intrusion-detection and fill their logs with "attacks." This is expected. That is exactly why the RoE includes emergency contacts and why you share your source IPs and time window up front — so the blue team can tell your authorized noise from a real intruder. Coordinating this in advance prevents a costly false-alarm incident response and keeps your activity distinguishable from a genuine threat.
Evidence handling: store evidence encrypted, transfer it only over the agreed secure channel, never in public chats or issue trackers, and delete it on the schedule the contract specifies.
Concrete authorized use case. A retailer hires your firm for a two-week web-application penetration test. The signed RoE lists three domains and one /24 IP range as in scope, a 09:00–18:00 UTC window, and forbids denial-of-service. On day three you find SQL injection in a search box. A junior instinct is to dump the customer table to "prove" it; the professional move is to run SELECT version() plus read one redacted row, screenshot it, log the decision ("PoC sufficient, no bulk extraction"), and move on. Later you notice a web shell with modification times two months before your engagement — a prior real breach. You stop, preserve it, and call the client's emergency contact within minutes. Both moves — the restrained PoC and the immediate escalation — are what the client is paying for.
Best-practice habits.
| Habit | Beginner | Advanced |
|---|---|---|
| Validation | Check the allow-list before every target | Automate a scope-gate (like may_test) into your tooling so off-scope actions are blocked, not just discouraged |
| Least privilege | Access only what a finding needs | Use scoped, time-boxed test accounts; avoid touching real user data at all where a synthetic account suffices |
| Secure defaults | Assume out-of-scope until proven in-scope; fail closed | Codify "fail closed" everywhere; require a second person to approve any destructive test |
| Logging | Keep a timestamped decision log | Correlate your log with the client's SOC so authorized noise is pre-cleared and abuse stands out |
| Error handling | Stop and ask when unsure | Rehearse stop-condition drills so escalation is fast and calm under pressure |
Outside formal engagements: if you find a bug in open-source software or a public product, use coordinated disclosure — report privately (a security contact, security.txt, or a bug-bounty program), give time to fix, and never publish a working exploit against unpatched systems. Reading source or testing your own local copy is fine; testing someone else's live server is not authorized.
All tasks are lab-only — run them against systems you own, local containers, or intentionally-vulnerable VMs/CTFs, never third-party systems. Each ends with a defensive conclusion.
Authorization checklist for every task below: (1) the target is a machine you own or a deliberately-vulnerable lab image; (2) it runs on localhost/an isolated network; (3) you have written the scope and window in your own notes before starting; (4) you know how you would "escalate" (here, just log it). Cleanup/reset: delete any marker files you create, revert VM snapshots, and clear evidence per your own retention note when done.
Task 1 — Write your own scope gate.
may_test idea from scratch.(allowed, reason); check a hardcoded allow-list, a time window, and a "destructive allowed" flag; fail closed on anything unexpected.ipaddress.ip_network for the allow-list; default unknowns to False.Task 2 — Design a decision-log line.
Task 3 — Add stop-condition detection to your gate.
breach_indicator=True; when set, return (False, "STOP: escalate — possible prior breach") regardless of other checks; write a fake decision log showing the halt + "escalation."Task 4 — Minimal-PoC policy.
version() string."Task 5 — Coordinated-disclosure playbook.
security.txt / bounty program), a reasonable fix-time expectation, what evidence to include, what you will NOT publish and why, and how to correct the misconception that "finding the bug lets me test other people's live servers."Main concepts. Authorized testing rests on four non-negotiables — authorization (current, signed, from someone empowered), scope (an allow-list; reachable is not authorized), do no harm (prefer a minimal proof-of-concept over full exploitation), and protect data (minimize access, never exfiltrate, secure or delete evidence). Layered on top are stop conditions (halt and escalate immediately on a prior real breach, a safety-critical issue, an outage, or scope drift) and coordinated disclosure for issues found outside an engagement.
Key artifacts/commands. A before-you-touch check (authorization? in scope? technique permitted? emergency contact known?), a fail-closed scope gate like may_test that answers "am I permitted?" not "can I connect?", and a timestamped decision log (UTC time, source, target, action, result, security decision, correlation id).
Common mistakes. Treating reachability as permission; poking a discovered real breach instead of escalating; dumping whole datasets to "prove" a bug; relying on verbal permission; and overclaiming — decoding is not verifying, a passing scanner is not proof of security, and nothing is ever "completely secure."
What to remember. The difference between professional security work and a crime is permission and restraint, and you must be able to prove both. Log what matters, never log secrets or full sensitive records, coordinate your noise with the client's defenders, and know exactly when — and how — to stop.