File size: 42,205 Bytes
4b427d3 6f8b6cb 1864ff4 6f8b6cb 1864ff4 6f8b6cb 1864ff4 6f8b6cb 1864ff4 6f8b6cb 4b427d3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 | <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PCI DSS Log requirements — check Python logs for card data</title>
<meta name="description" content="This browser tool flags Python logging, print, and raise calls that write sensitive data into logs, with a PCI DSS requirement and fix for each line.">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/python-pci-log-leak-lint"></head>
<body>
<div class="wrap">
<h1>PCI DSS Log requirements — check Python logs for card data</h1>
<p class="lede">Flags Python logging, print and raise calls that write card numbers, CVV, track data, PINs, passwords or auth headers into logs, with the PCI DSS v4.0.1 requirement and a fix per line. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">import logging
import stripe
from flask import Flask, request
app = Flask(__name__)
logger = logging.getLogger("payments")
@app.route("/pay", methods=["POST"])
def pay():
logger.info("incoming payment payload: %s", request.json)
data = request.get_json()
card_number = data["card_number"]
cvv = data["cvv"]
logger.debug("charging card %s", card_number)
try:
charge = stripe.Charge.create(amount=data["amount"], currency="usd", source=data["token"])
except stripe.error.CardError as e:
logger.error(f"card declined, cvv={cvv}: {e}")
raise
return {"id": charge.id}
@app.route("/terminal", methods=["POST"])
def terminal():
track2 = request.form["track2"]
print("swipe received", track2)
return "ok"
@app.route("/login", methods=["POST"])
def login():
user = request.form["user"]
password = request.form["password"]
if not check_login(user, password):
logger.warning("login failed for %s using %s", user, password)
logger.debug("auth headers: %s", request.headers)
return "ok"</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="paid"><strong>Free here:</strong> Checks the open Python file against 10 rules and lists every log, print or raise line that writes card data or credentials, with line number, PCI DSS v4.0.1 requirement and fix. Complete for free.. <strong>In the editor:</strong> Scans every .py file in the repository in one run and exports the findings as a Markdown evidence table for your PCI DSS assessment. — that part asks for a licence key.</div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var PLL_RULES = [
{
"id": "pan-in-log",
"sev": "error",
"kind": "sink",
"code": "(?\u003c![a-z])(card_?num(ber)?|card_?no|cc_?num(ber)?|credit_?card(_?num(ber)?)?|pan|primary_?account(_?number)?|account_?number)(?![a-z])",
"unless": "\\[\\s*-4\\s*:\\s*\\]|last_?4|last_?four|mask|truncat|redact",
"req": "PCI DSS v4.0.1 Req 3.5.1 (PAN rendered unreadable anywhere it is stored, logs included)",
"msg": "A card number (PAN) variable reaches a log, print or exception message.",
"fix": "log card_number[-4:] or the processor token, never the full PAN"
},
{
"id": "cvv-in-log",
"sev": "error",
"kind": "sink",
"code": "(?\u003c![a-z])(cvv2?|cvc2?|cvn|csc|security_?code|card_?code)(?![a-z])",
"req": "PCI DSS v4.0.1 Req 3.3.1.2 (card verification code not retained after authorization)",
"msg": "The card verification code (CVV/CVC) reaches a log, print or exception message.",
"fix": "drop the field before any logger or error handler sees it; log nothing about it"
},
{
"id": "track-in-log",
"sev": "error",
"kind": "sink",
"code": "(?\u003c![a-z])(track_?[12]|track_?data|mag_?stripe|swipe_?data)(?![a-z])",
"req": "PCI DSS v4.0.1 Req 3.3.1.1 (full track data not retained after authorization)",
"msg": "Magnetic-stripe track data reaches a log, print or exception message.",
"fix": "pass track data straight to the processor and never write it anywhere"
},
{
"id": "pin-in-log",
"sev": "error",
"kind": "sink",
"code": "(?\u003c![a-z])(pin|pin_?block|pin_?code)(?![a-z])",
"req": "PCI DSS v4.0.1 Req 3.3.1.3 (PIN and PIN block not retained after authorization)",
"msg": "A PIN or PIN block reaches a log, print or exception message.",
"fix": "remove the PIN from the message; log only that PIN entry happened"
},
{
"id": "password-in-log",
"sev": "error",
"kind": "sink",
"code": "(?\u003c![a-z])(passw(or)?d|pwd|passphrase)(?![a-z])",
"req": "PCI DSS v4.0.1 Req 8.3.2 (authentication factors unreadable during transmission and storage)",
"msg": "A password value reaches a log, print or exception message.",
"fix": "log the user id and the outcome only, never the password"
},
{
"id": "auth-header-in-log",
"sev": "error",
"kind": "sink",
"code": "(?\u003c![a-z])(authorization|bearer|api_?key|secret(_?key)?|client_?secret|access_?token|refresh_?token)(?![a-z])|\\.headers(?![a-z_])",
"unless": "mask|redact",
"req": "PCI DSS v4.0.1 Req 8.3.2 (authentication factors unreadable during transmission and storage)",
"msg": "Request headers, an API key or a bearer token reach a log, print or exception message.",
"fix": "log a header allow-list (e.g. content-type, request id) instead of request.headers"
},
{
"id": "request-dump-in-log",
"sev": "warning",
"kind": "sink",
"code": "request\\.(json|data|body|form|POST|GET|values|get_json\\s*\\()|json\\.dumps\\(\\s*(request|payload|body|data)\\b",
"req": "PCI DSS v4.0.1 Req 3.3.1 (sensitive authentication data not retained after authorization)",
"msg": "A whole request body is logged; on a payment route it carries PAN and CVV.",
"fix": "log named, non-sensitive fields (order id, amount, currency) instead of the body"
},
{
"id": "locals-dump-in-log",
"sev": "warning",
"kind": "sink",
"code": "locals\\s*\\(\\s*\\)|(?\u003c![a-z_])vars\\s*\\(|__dict__",
"req": "PCI DSS v4.0.1 Req 3.3.1 (sensitive authentication data not retained after authorization)",
"msg": "locals(), vars() or __dict__ is logged, which writes every variable in scope, card fields included.",
"fix": "log the two or three fields you need by name"
},
{
"id": "pan-literal-in-log",
"sev": "error",
"kind": "luhn",
"req": "PCI DSS v4.0.1 Req 3.5.1 (PAN rendered unreadable anywhere it is stored, logs included)",
"msg": "A 13-19 digit number that passes the Luhn check sits inside a log, print or exception message.",
"fix": "replace it with the last four digits; keep test card numbers out of log text"
},
{
"id": "sentry-default-pii",
"sev": "warning",
"kind": "line",
"code": "send_default_pii\\s*=\\s*True",
"req": "PCI DSS v4.0.1 Req 3.3.1 and 3.5.1 (card data sent to a third-party error tracker is stored there)",
"msg": "Sentry is initialised with send_default_pii=True, which attaches headers, cookies and user data to every event.",
"fix": "set send_default_pii=False and scrub card fields in before_send"
}
]
;</script>
<script>/* python-pci-log-leak-lint — finds card data and credentials written to Python logs. Same file runs in node (VS Code) and in the browser. */
(function () {
var RULES = (typeof module !== 'undefined' && typeof require !== 'undefined') ? require('./rules.json') : window.PLL_RULES;
// A statement that writes text somewhere a human or a log store will keep it.
var SINK = /(?:\b(?:logger|logging|log|_logger|_log|LOGGER|LOG|self\.log(?:ger)?|app\.logger|current_app\.logger)\.(?:debug|info|warning|warn|error|exception|critical|fatal|log)\s*\(|(?<![\w.])print\s*\(|\braise\s+[A-Za-z_][\w.]*\s*\()/;
var STR = /([rRbBuUfF]{0,2})("""[\s\S]*?"""|'''[\s\S]*?'''|"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*')/g;
// Keep code and f-string {expressions}; drop plain message text so "password reset for %s" is not a leak.
function codeOnly(s) {
return s.replace(STR, function (m, pre, body) {
if (!/f/i.test(pre)) return '""';
var out = [], re = /\{([^{}]*)\}/g, x;
while ((x = re.exec(body))) out.push(x[1]);
return '"' + (out.length ? ' ' + out.join(' ') + ' ' : '') + '"';
});
}
function dropComment(line) {
var s = line.replace(STR, function (m) { return m.replace(/#/g, '_'); });
var i = s.indexOf('#');
return i < 0 ? line : line.slice(0, i);
}
function depth(s) {
var c = codeOnly(s);
return (c.match(/\(/g) || []).length - (c.match(/\)/g) || []).length;
}
function luhn(d) {
var sum = 0, alt = false;
for (var i = d.length - 1; i >= 0; i--) {
var n = +d[i];
if (alt) { n *= 2; if (n > 9) n -= 9; }
sum += n; alt = !alt;
}
return sum % 10 === 0;
}
function parseDay(s) {
var m = String(s || '').match(/(\d{4})-(\d{2})-(\d{2})/);
if (!m) return null;
var d = Date.UTC(+m[1], +m[2] - 1, +m[3]);
return isNaN(d) ? null : d;
}
// Req 10.5.1 keeps audit log history for at least 12 months.
function tail(today) {
var t = parseDay(today);
if (t === null) return '';
var d = new Date(t), until = new Date(Date.UTC(d.getUTCFullYear() + 1, d.getUTCMonth(), d.getUTCDate()));
return ' Written on ' + d.toISOString().slice(0, 10) + ', a log line kept for the 12 months of Req 10.5.1 is still stored on ' + until.toISOString().slice(0, 10) + '.';
}
// Each sink statement, flattened to one line even when the call wraps.
function statements(lines) {
var out = [];
for (var i = 0; i < lines.length; i++) {
var ln = dropComment(lines[i]);
if (!/\S/.test(ln) || !SINK.test(ln)) continue;
var text = ln.slice(ln.search(SINK)), j = i, bal = depth(text);
while (bal > 0 && j + 1 < lines.length && j - i < 8) {
j++;
var nx = dropComment(lines[j]);
text += ' ' + nx.trim();
bal += depth(nx);
}
out.push({ line: i + 1, raw: text.replace(/\s+/g, ' '), code: codeOnly(text).replace(/\s+/g, ' ') });
}
return out;
}
function check(text, opts) {
opts = opts || {};
var lines = String(text || '').split(/\r?\n/), sinks = statements(lines), findings = [], end = tail(opts.today);
RULES.forEach(function (r) {
var add = function (line, extra) {
findings.push({ check: r.id, sev: r.sev, line: line, msg: r.msg + (extra || '') + ' ' + r.req + ' → ' + r.fix + '.' + end });
};
if (r.kind === 'sink') {
var re = new RegExp(r.code, 'i'), un = r.unless ? new RegExp(r.unless, 'i') : null;
sinks.forEach(function (s) {
if (re.test(s.code) && !(un && un.test(s.code))) add(s.line, '');
});
} else if (r.kind === 'luhn') {
sinks.forEach(function (s) {
var m, dg = /(?<!\d)\d(?:[ -]?\d){12,18}(?!\d)/g;
while ((m = dg.exec(s.raw))) {
var d = m[0].replace(/\D/g, '');
if (d.length >= 13 && d.length <= 19 && luhn(d)) { add(s.line, ' Found ending ' + d.slice(-4) + '.'); break; }
}
});
} else if (r.kind === 'line') {
var lr = new RegExp(r.code);
lines.forEach(function (ln, i) { if (lr.test(dropComment(ln))) add(i + 1, ''); });
}
});
findings.sort(function (a, b) { return a.line - b.line; });
return { findings: findings };
}
var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
if (typeof module !== 'undefined' && module.exports) module.exports = API;
if (typeof window !== 'undefined') window.PLLENGINE = API;
})();
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
function summaryLine(res) {
const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
const f = (res && res.findings) || [];
const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); });
return lines.join('\n');
}
function toHtml(res) {
const f = (res && res.findings) || [];
if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var PLLREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = PLLREPORT;
else window.PLLREPORT = PLLREPORT;
</script>
<script>
(function(){
var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
today.value=new Date().toISOString().slice(0,10);
function run(){ try { var res=window.PLLENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.PLLREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
go.onclick=run; if (src.value.trim()) run();
})();
</script>
<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://open-vsx.org/extension/readystack/python-pci-log-leak-lint" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'python-pci-log-leak-lint',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind Python Log Leak Lint: PCI DSS Card Data every day. When a rule changes, you get a single email with what changed and the updated check. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"python-pci-log-leak-lint",lang:"en",source:"rule_alert"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section>
<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Finds card numbers, CVV, track data, PINs, passwords and auth headers in Python log, print and raise calls, with 10 rules</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">Semgrep Teams, a general code-scanning service, lists $35 per contributor per month once a team is past its free 10-contributor tier</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_9woUkFPMhTLykmWudXCRCULpNExlW6G00mSVb0jkiu0" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>
<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&","<":"<",">":">",'"':"""}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div><section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-25">Updated 2026-09-25</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-25","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku239038_result_card.jpg" alt="Python Log Leak Lint: PCI DSS Card Data" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>6 PCI DSS leaks in Python logs: that is what one Flask payment handler produced when I checked it on 2026-09-24. If you are a Python backend developer at a card-accepting merchant, payment provider or fintech, and a PCI DSS v4.0.1 assessment is coming, this is the file to look at first.</p>
<h3>The file</h3>
<p>The handler is short: a /pay route that reads a JSON body and calls Stripe, a /terminal route that receives a card swipe, and a /login route. Here are the 6 findings, with line numbers:</p>
<ul><li>L12 request-dump-in-log: <code>logger.info("incoming payment payload: %s", request.json)</code> logs the whole body. On a payment route it carries the PAN and CVV. Req 3.3.1.</li><li>L16 pan-in-log: <code>logger.debug("charging card %s", card_number)</code> writes the full PAN. Req 3.5.1 says a PAN must be unreadable anywhere it is stored, and logs count.</li><li>L20 cvv-in-log: <code>logger.error(f"card declined, cvv={cvv}: {e}")</code> puts the card verification code into an error log. Req 3.3.1.2 says it must not be retained after authorization.</li><li>L28 track-in-log: <code>print("swipe received", track2)</code> writes track 2 data to stdout. Req 3.3.1.1.</li><li>L37 password-in-log: a failed login logs the password it was given. Req 8.3.2.</li><li>L38 auth-header-in-log: <code>logger.debug("auth headers: %s", request.headers)</code> logs the Authorization header. Req 8.3.2.</li></ul>
<p>The fixed version of the same file logs the order id, the amount and <code>card_number[-4:]</code>. It gives 0 findings.</p>
<h3>What you lose, and when</h3>
<p>A leaked line does not go away when you fix the code. Req 10.5.1 asks you to keep audit log history for at least 12 months. A line written on 2026-09-24 is still stored on 2027-09-24. Every log store that holds it is in scope for the assessment.</p>
<h3>Why the free options stop short</h3>
<p>General Python linters check style, and some flag hard-coded passwords. None of them tells you that <code>cvv</code> inside an f-string in <code>logger.error</code> is a Req 3.3.1.2 problem. A chat assistant can explain the requirement, but it gives no line numbers in your file.</p>
<h3>How the check reads code</h3>
<p>Plain message text is ignored. <code>logger.info("password reset for %s", user)</code> is not a finding, because the password is not in the values. Only the arguments, the f-string <code>{expressions}</code> and concatenations count. A call that wraps over several lines is joined first. There are 10 rules: PAN, CVV, track data, PIN, password, auth headers, whole request bodies, <code>locals()</code> dumps, Luhn-valid numbers inside log text, and <code>send_default_pii=True</code> in Sentry.</p>
<p>The date matters. Check a point-of-sale handler on 2026-12-01 that logs <code>track_data</code> and <code>pin_block</code>, dumps <code>locals()</code> and prints a test card number, and you get 4 findings, each stored until 2027-12-01 under the 12-month retention. A refund module with Sentry's <code>send_default_pii=True</code>, a <code>raise ValueError</code> that includes <code>card.pan</code>, and a logged <code>STRIPE_API_KEY</code> gives 3.</p>
<h3>Yardstick</h3>
<p>Semgrep Teams, a general code-scanning service, lists $35 per contributor per month once a team is past its free 10-contributor tier. This lint does one narrow job and cites the requirement per line.</p>
<h3>Free and full</h3>
<p>The free version checks the open .py file against all 10 rules and lists every leaking line with its requirement and fix, in VS Code or in the browser. For one file, that finishes the job. The full version checks every .py file in the repository in one run and exports a Markdown evidence table for the assessment.</p>
<p>It does not prove compliance and does not follow data across files. Paste your payment handler in and see what it writes to your logs.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku239038_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku239038_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Python Log Leak Lint do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It checks every logger, logging, print and raise statement in a Python file against 10 rules. For each line that writes card data or credentials, it names the PCI DSS v4.0.1 requirement and gives a fix. A Flask payment handler gave 6 findings: request.json logged whole, card_number, cvv in an f-string, print of track2, password and request.headers. The fixed file gave 0.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is Python Log Leak Lint for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It is for Python backend developers at card-accepting merchants, payment service providers and fintechs who use Flask, Django, FastAPI or Celery and are preparing for a PCI DSS v4.0.1 assessment. It also suits reviewers checking a payment handler that an AI coding assistant wrote, before that code writes a CVV or card number into production logs.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not use a free Python linter or a chatbot?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">General Python linters check style, and some check for hard-coded passwords. They do not map a CVV, track data, a PIN block or a Luhn-valid card number inside a log call to PCI DSS Req 3.3.1 or 3.5.1. A chatbot can explain the rule, but it gives no line numbers in your own file.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free, and what does the full version add?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">The free version checks the open Python file against all 10 rules and lists every leaking line with its line number, PCI DSS requirement and fix. For one file, that is the whole job. The full version is $29 once for a licence key. It checks every .py file in the repository in one run and exports a Markdown evidence table for the assessment.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does the alternative cost?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Semgrep Teams, a general code-scanning service, lists $35 per contributor per month once a team is past its free 10-contributor tier. That service covers far more than logging. This lint does one narrow job: it finds the log, print and raise lines that write card data or credentials, and cites the PCI DSS v4.0.1 requirement for each one.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Python Log Leak Lint do?", "acceptedAnswer": {"@type": "Answer", "text": "It checks every logger, logging, print and raise statement in a Python file against 10 rules. For each line that writes card data or credentials, it names the PCI DSS v4.0.1 requirement and gives a fix. A Flask payment handler gave 6 findings: request.json logged whole, card_number, cvv in an f-string, print of track2, password and request.headers. The fixed file gave 0."}}, {"@type": "Question", "name": "Who is Python Log Leak Lint for?", "acceptedAnswer": {"@type": "Answer", "text": "It is for Python backend developers at card-accepting merchants, payment service providers and fintechs who use Flask, Django, FastAPI or Celery and are preparing for a PCI DSS v4.0.1 assessment. It also suits reviewers checking a payment handler that an AI coding assistant wrote, before that code writes a CVV or card number into production logs."}}, {"@type": "Question", "name": "Why not use a free Python linter or a chatbot?", "acceptedAnswer": {"@type": "Answer", "text": "General Python linters check style, and some check for hard-coded passwords. They do not map a CVV, track data, a PIN block or a Luhn-valid card number inside a log call to PCI DSS Req 3.3.1 or 3.5.1. A chatbot can explain the rule, but it gives no line numbers in your own file."}}, {"@type": "Question", "name": "What is free, and what does the full version add?", "acceptedAnswer": {"@type": "Answer", "text": "The free version checks the open Python file against all 10 rules and lists every leaking line with its line number, PCI DSS requirement and fix. For one file, that is the whole job. The full version is $29 once for a licence key. It checks every .py file in the repository in one run and exports a Markdown evidence table for the assessment."}}, {"@type": "Question", "name": "What does the alternative cost?", "acceptedAnswer": {"@type": "Answer", "text": "Semgrep Teams, a general code-scanning service, lists $35 per contributor per month once a team is past its free 10-contributor tier. That service covers far more than logging. This lint does one narrow job: it finds the log, print and raise lines that write card data or credentials, and cites the PCI DSS v4.0.1 requirement for each one."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:239038,slug:"python-pci-log-leak-lint",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/sql-card-data-lint"><span class="yj-rel-t">SQL Card Data Lint (PCI DSS Req 3)</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/firestore-rules-guard"><span class="yj-rel-t">Firestore Rules Guard</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/pci-payment-page-script-audit"><span class="yj-rel-t">PCI Payment Page Script Audit: requirement 6.4.3 and 11.6.1 on your checkout m</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/log-pii-telemetry-lint"><span class="yj-rel-t">PII-in-Logs Lint (console.log(req.body))</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/docker-image-for-data-science"><span class="yj-rel-t">Docker Image for Data Science — Reproducible JupyterLab + MLflow Stack (Python</span><span class="yj-rel-p">$19</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/robots-txt-ai-crawler-audit"><span class="yj-rel-t">AI Crawler Rules - robots.txt Audit for AI Search</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>
|