deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Cloudflare blocked Python's default urllib User-Agent, breaking licence checks for two months

A Python tool author says Cloudflare's edge silently rejected urllib's default User-Agent with error 1010, breaking online licence activation for two months before a customer noticed.

Cloudflare blocked Python's default urllib User-Agent, breaking licence checks for two months

What happened

The author of pyobfus, a commercial Python code obfuscator, has documented on dev.to how online licence activation for the product failed silently for roughly two months. According to the post, the last successful online verification on record was 8 July, and the breakage only surfaced on 12 September, when a customer who had bought a Pro licence emailed about an activation error reading "Access denied". It soon became clear that online activation was failing for every customer, on every platform, across every release shipped up to that point.

Cloudflare rejected urllib's default signature

The licence client was built on Python's urllib.request and never set a User-Agent header, so every activation request identified itself with urllib's default string, Python-urllib/3.x. The licence server runs as a Cloudflare Worker, and the author reports that Cloudflare's edge rejected those requests before they reached the Worker, answering with HTTP 403 and a plain-text body containing "error code: 1010", which Cloudflare documents as a block based on browser signature.

Testing showed the rule was narrow rather than a blanket ban on non-browser clients. Requests signed as python-requests/2.x, requests carrying a browser-style string, and requests with no User-Agent header at all were passed through to the Worker and received its normal JSON reply. Only the urllib default was refused.

Why two months passed without an alarm

The author attributes the delay to two gaps on the client side. First, the client expected JSON responses; when it received a plain-text 403 it could not parse, it discarded the body and surfaced a hard-coded "Access denied" message that customers would reasonably read as a bad licence key. Second, nothing was watching the activation path. The only failure signal was a customer deciding to write in.

Digging deeper turned up older defects in the same code. The device fingerprint included platform.release(), so a routine OS update gave a machine a new identity, invalidated the local cache and consumed another of the three device slots. The fingerprint also relied on uuid.getnode(), which returns a random value when Python cannot read a hardware address. Separately, a server-side revocation could be absorbed by the offline-cache fallback, leaving the client to report the licence as valid.

What the fix changed

The fixes shipped in pyobfus 0.5.26 on 13 September. The client now sends a User-Agent that starts with pyobfus-license/ followed by its version number. Error handling distinguishes an infrastructure block from a licence rejection: a 403 without the Worker's expected JSON is treated as a network-level failure, with a message naming the offline registration command customers can use instead.

A scheduled GitHub Actions workflow now probes production twice a day by calling the real endpoint through the same client function customers use, sending a well-formed licence key that was never issued. The probe checks that the response body matches the expected unknown-key JSON, so a missing route or a proxy's 404 page cannot pass as healthy. The author validated it by forcing it red in several ways, including restoring the old User-Agent and pointing it at an unreachable endpoint.

Cache handling was also rewritten. A signed cache now survives changes in device identity, while an explicit revoked or expired answer from the server blocks the fallback. The device ID is a random value generated once and stored in the user's home directory, and a fourth registration on the server replaces the least recently verified device. Because already-installed copies keep sending the blocked header until users upgrade, the author also emailed known-affected customers before the release went out.

Why it matters

urllib's default User-Agent is one of the most common signatures on the Python side of the internet, and the developer shipping a tool has no control over how a CDN in front of an API treats it. As this incident shows, an edge policy change can cut off a production integration with no deploy on either side, no error page and no alert, while a generic fallback message disguises an infrastructure failure as a user problem. The practical takeaways are simple: set an explicit User-Agent on anything that phones home, surface the real status code and response shape when parsing fails, and probe the live path with a harmless input so the next silent block is caught in hours rather than months.

  • #python
  • #cloudflare
  • #urllib
  • #user-agent
  • #http
  • #licensing

Related posts