jazzband

Activity

Stars and mentions · Last 30 days, Oct 5 – Nov 3

GitHub stars14
Mentions6
02468Oct 5Oct 19Nov 3
UTC days · repositories jazzband owns2 repositories

Top repositories

jazzband’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside jazzband’s account where jazzband opened or reviewed pull requests in the past 365 days.

Who noticed jazzband

Everyone who starred or forked jazzband’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

6posts
Nov 40 posts
  1. Bluesky · · @jezdez.dev

    In week 2 (October 20 to 27), another 14 moved to new homes, including 10 to Django Commons. That brings the total to 29 of the 82 projects we're tracking. Of those 29, 13 handoffs are complete and 16 still need release, docs or access cleanup. Thanks to everyone for helping with the moves!

    3 likes · 1 repost
  2. Pillow will open your avatar.png as a Photoshop file --- title: "Pillow will open your avatar.png as a Photoshop file" published: true description: "Pillow picks the decoder from a file's bytes, not its name. An allowlist keeps out 18 of 24 upload-reachable bugs from this year's flood, but not the two worst." tags: "python, security, django, pillow" canonical_url: "blog.peng.ly" cover_image: "blog.peng.ly" ai_assisted: true --- I saved a Photoshop document, renamed it avatar.png, and posted it to a Django form that only accepts images. It went through. Then three different thumbnail libraries each opened it, ran Pillow's Photoshop plugin, and read its pixels back out. Nothing there is a bug on its own. It's how Pillow is meant to work: Image.open sniffs the first few bytes and picks a decoder by what it finds, not by the extension and not by the Content-Type. A PSD header gets the PSD plugin whatever the file's called. The name avatar.png is decoration. That didn't bother me much until this month, when Pillow had the worst few weeks of its life. 60 advisories in a year, 39 of them in three days !Pillow advisories by year published Pillow advisories by year published (the data) _GitHub's advisory database, pip ecosystem, by publish date, as of 24 October. Publish year isn't the same as when the bug landed - a few 2026 entries are old CVEs only now written up._ One in 2025. Two in 2024. Sixty so far in 2026, and 39 of those went out on 20 to 22 October in one batch. Four of the first batch were critical, and another critical landed on the 23rd while I was still counting. If you run anything that takes image uploads in Python, you got the upgrade alert this week. The fix is to upgrade, and you should, today. But I wanted to know what the avatar.png trick costs you in the meantime, because the Pillow handbook has a mitigation sitting right there and nobody's said how much it actually buys. formats= shuts the door, mostly Image.open takes an optional formats= argument: a list of the plugins it's allowed to try. Leave it off and it tries all of them. Pass the handful you serve and the rest stop existing, header or no header. from PIL import Image Image.open(upload) # tries every plugin Image.open(upload, formats=["JPEG", "PNG", "GIF", "WEBP"]) # these four only With that second line, my Photoshop avatar.png raises UnidentifiedImageError instead of handing back a PsdImageFile. So I went through all 60 advisories by hand, sorted them by how an attacker reaches the vulnerable code, and checked each one against that four-format allowlist. !How you reach this year's 60 Pillow bugs How you reach this year's 60 Pillow bugs (the data) _Hand-classified from each advisory's text and PoC on 24 October. "Opening the upload" means Image.open plus a normal load, seek, getexif or thumbnail._ Only 24 of the 60 are reachable just by opening an uploaded file. The rest need you to do something to it first: re-save it, convert it, run a filter, render a font. More on those in a minute, because the allowlist does nothing for them. Of those 24, the four-format allowlist keeps out the vulnerable plugin for 18 (a few of those need an odd setup to fire anyway - a real filename, the optional olefile package, Windows). Most of the open-an-upload bugs this year are in plugins you can switch off and never miss: FITS astronomy images, DOS-era SPIDER files, FlashPix, McIdas satellite data, and a dozen more. Not the two you'd most want gone Here's where it stops being a happy story. Six of the 24 get through the allowlist, and they're the six you can't switch off, because they're the formats you actually serve. | Gets through | Why | | --------------------------------- | --------------------------------------------------------------------------------- | | JPEG heap overflow (critical) | It's JPEG. The advisory says outright that formats=["JPEG"] doesn't prevent it. | | Multi-frame stale read (critical) | Reached through MPO, and MPO rides the JPEG plugin. | | A second MPO bug | Same door. | | PNG eXIf | It's PNG, read on getexif. | | APNG | It's PNG. | | A GIF decode bug | It's GIF. | The MPO one is the sly bit. MPO is the multi-picture format your phone saves for its depth and burst shots, and Pillow has no separate detector for it. A JPEG with an extra marker gets quietly promoted to an MpoImageFile by the JPEG plugin itself: >>> im = Image.open(phone_photo, formats=["JPEG"]) >>> type(im).__name__, im.n_frames ('MpoImageFile', 2) >>> Image.open(phone_photo, formats=["MPO"]) KeyError: 'MPO' So you can't allow JPEG and refuse MPO. Asking for ["MPO"] on its own is a KeyError, because there's nothing to register. If you need the MPO bug shut, the only lever is to check im.format after opening and reject anything that isn't what you wanted, knowing the header's already been parsed by the time you can look. Two of this year's criticals are stopped by the allowlist, and both come with an asterisk: the FlashPix one needs olefile installed, the Ghostscript one needs a filename the attacker chose. The two criticals that need nothing but a plain Image.open are the JPEG overflow and the MPO read, and the allowlist can't touch either. It doesn't help after the open, either That avatar.png from the start did get decoded three times, remember. I wired up a one-file Django app with easy-thumbnails, sorl-thumbnail and django-imagekit, uploaded a benign file in each niche format as avatar.png, and traced every load Pillow ran. All three thumbnailers decoded all of them with their own plugin: the PSD body through the raw PSD decoder, FITS through FITS, JPEG2000 through OpenJPEG, right down to reading back the correct pixel. easy-thumbnails even walked to the second MPO frame. None of those three passes formats=, and neither does the thing that let the file in. Django's own ImageField is this, in django/forms/fields.py: image = Image.open(file) No allowlist. It parses the header to confirm the upload is _an_ image, sets the content type from whatever Pillow detected, and saves it. Wagtail and Saleor sniff the MIME type first and are a bit better off; most of the rest I looked at open it bare. And the whole second half of the advisory pile - 14 save bugs, 8 operations, the fonts - sits past the point an allowlist can see. Those fire when you re-save the upload as a different format, resize it, apply a colour profile, re-encode it as a thumbnail. Which is exactly what an avatar pipeline does to every file, one plugin or forty. A crafted JPEG that's fine to open but overflows when it's re-saved with an ICC profile and a comment doesn't care which formats you allowed on the way in. So: upgrade Pillow first, that's the actual fix for all 60. If you take image uploads, also pass formats= with the few you serve - it's one argument and it retires a decent chunk of the attack surface for free. And if you need the MPO door shut too, check im.format after opening and reject anything you didn't ask for. Just don't file any of it under "fixed". The JPEG you have to allow is still the JPEG with the hole in it.

  3. Watson is here to help you manage your time. You want to know how much time you are spending on your projects? You want to generate a nice report for your client? Watson is here for you. jazzband/Watson t.co

    93 views
  4. Fail-closed or fail-open? Decide per side effect, then check what your cache already decided Fail-closed: when something a decision depends on is unavailable, the answer is no. Fail-open: the answer is yes. Fail-bounded: the answer is yes, inside a limit that does not need the missing dependency. "Fail-closed or fail-open" is the wrong question at the scale of a service. Ask it per dependency and per kind of request. And before you answer, check your cache client: ours was configured to swallow errors, which turns every security decision built on it into fail-open without anyone deciding so. Why two words are not enough The third definition has no standard name, and it is the one we use most. OWASP's principle is called fail securely, and it is usually read as "always closed". For a service that sits between customer apps and third-party APIs, always closed means that a cache outage takes every app's weather widget down with it. Always open means that the same outage removes the only brake on a connector that can charge a card. Both camps are right about their half of the traffic. So the unit of decision cannot be the service. The table The proxy behind this table is one we are trialling at GoodBarber. It keeps third-party API keys on the server for widgets that run inside customer apps, and today it serves a few test tenants. Its security specification settles the question in one decision, written before the code: the degraded mode depends on the side effect. A connector is registered either as read-only or as side-effecting, next to its host and its allowed methods, and every dependency gets a row. Everything below is conditional on that flag being right. The proxy applies the column it is given. It does not infer the class from the traffic, so a connector wrongly registered as read-only gets the lenient column. | When this is unavailable | Read-only connector | Side-effecting connector | |---|---|---| | The keys that verify token signatures | refused, 503 | refused, 503 | | The rate limiter's shared store | allowed, inside a per-process ceiling | refused, 503 | | The kill switch | cannot be unavailable on its own: it lives in the primary database and is read on every request | same | The first row has no nuance. A signature you cannot check is not a valid signature, whatever the request does. The status code matters as much as the refusal: 503, never 401. A 401 tells the client its token is bad, and a well-behaved client responds by asking for a new one, which turns an outage of the key endpoint into a stampede on the token endpoint. The third row is a placement decision. A kill switch that lives in a cache disappears exactly when the infrastructure is having a bad day, which is when you reach for it. Ours is a column in the database the request already needs. The option in the middle The second row did not start that way. In the first version, read-only traffic simply passed when the limiter's store was down. An adversarial review, run before the first commit of that code landed, flagged it, and the reason fits in one sentence. "Read-only" describes the effect on the third party, not the cost to us or to the customer's API quota. Fail-open on reads is still an unmetered pipe. The fix is a few lines, and it is the whole idea of fail-bounded: try: ok = hit(tenant) and hit(tenant, connector) and hit(tenant, host) # shared store except StoreUnavailable: if side_effecting: return FAIL_CLOSED # 503: no brake, no payment return ALLOW if local_window_allows(tenant, connector) else THROTTLED return ALLOW if ok else THROTTLED local_window_allows is a sliding one-second window kept in the worker's memory. It is cruder than the shared limiter and it needs nothing that can be down. It is also per process, so the real ceiling is that number multiplied by the number of workers. Reads keep flowing, and "how much" still has an answer. The setting that had already decided This is what I would check first in any codebase. Our Redis cache is configured with django-redis's IGNORE_EXCEPTIONS, which its documentation describes as memcached-like behaviour: when the server is unreachable, a read returns None and a write does nothing. For a cache, that is the right call. A cache that is down should look like a cache that is empty. A rate limiter built on that client inherits the setting. incr does not raise. It returns None. A naive comparison then either crashes or, written a little differently, treats "no answer" as "under the limit". Nobody chose fail-open. A convenience flag, set once for caching, chose it for every security decision stacked on the same client. A shared client's error policy is a security decision for everything built on top of it. So the limiter treats silence as a failure, explicitly: current = cache.incr(key) if current is None: # the client swallowed a backend error raise StoreUnavailable # silence is a failure, not "under the limit" One of the six tests of that module exists only for this case, and its name says so: none_from_ignore_exceptions_is_treated_as_unavailable. Here is the whole chain during an outage. Redis stops answering. incr raises nothing and returns None. The limiter calls that unavailable, and the table decides: 503 for a connector that can charge a card, a local ceiling for one that reads the weather.

    10 reactions
  5. Bluesky · · @jezdez.dev

    I've kicked off the wind-down work on Jazzband and have sent out emails to most project leads and people involved. In case there is a Jazzband project you care about, please check out the dedicated project board and engage: github.com/orgs/jazzband/projects/16/views/7 #python #django #django-commons #pypa

    5 likes · 1 repost
  6. Watson is a tool to track your time. You want to know how much time you are spending on your projects? You want to generate a nice report for your client? Watson is here for you. jazzband/Watson t.co

    1 like · 53 views

Achievements

Awards earned by the repositories jazzband owns. Select an award to embed its badge.

No awards yet

Repositories earn awards by reaching the top three in a completed trending day or week.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.