This is my third write up about contributing to WooCommerce. The first one was about a “good first issue” that taught me how a large codebase fits together. The second was about a block where I learned to read before I write. This one is different. This time the most important thing I learned had nothing to do with the feature I was fixing, and everything to do with a piece of malware that hijacked my machine and pushed itself into an open source project used by millions, under my name.
Let me tell the story from the start.
Why I picked this issue
After two merged PRs, I wanted something harder than a “good first issue”. I wanted a real bug, the kind where something is silently broken and you have to understand why.
The issue I found was exactly that. When you enable “Full Page Reload” on a Product Collection block, the product filters stop applying. You click a filter, and nothing happens. I liked it because it touched parts I hadn’t worked with before in this repo: server side rendering, the interactivity API, and the way two separate blocks talk to each other on the same page. So I commented on the issue (https://github.com/woocommerce/woocommerce/issues/64057), asked to be assigned, and started.
Naming the real problem
I carried the lesson from my last PR with me, which is to read before touching anything. So before writing a single line, I spent time
understanding how these blocks are connected.
That reading changed how I saw the whole thing. Product Collection and Product Filters are not parent and child. They are siblings on the page, linked only by a shared queryId, with no shared DOM parent between them. So this was not really a “filter bug”. It was a communication problem between two blocks that don’t have an easy way to talk to each other. Once I named it that way, I knew what kind of solution I was actually looking for.
The first fix, and the trade off I didn’t fully weigh
My first solution worked. On the server, the Controller marked the block with a forcePageReload flag. On the client, the navigate action read that flag, and if it was set, it did a full window.location.assign(url) instead of going through the interactivity router. I pushed it, opened the PR (https://github.com/woocommerce/woocommerce/pull/64131),
and Albert (Aljullu), the same maintainer who reviewed my earlier PRs, confirmed it. He wrote “Really nice work” and said the PR fixed the issue. But he also left a comment, and CodeRabbit raised the same point. I had passed the flag using wp_interactivity_config, which is a global. That meant one Product Collection’s setting would leak into every Product Filters block on the page. My fix made the bug go away, but it wasn’t correct. This is something I keep relearning: making a bug disappear and solving it properly are two different things. I had chosen the global config because it was the fastest path to a working result, and I had quietly pushed the real cost into the future.
I was getting ready to fix that. And then something much stranger happened.
A nation state worm hijacked my pull request
My .gitignore had quietly stopped ignoring .env, and my postcss.config.js had an obfuscated payload added to it that ran at import time. I stared at it. I had not written that. I had not even opened those files.
What I had on my machine was something called PolinRider. It is a supply chain attack attributed to the Lazarus Group, and by the time I ran into it, it had already infected almost two thousand GitHub repositories. I picked it up from a teammate on a completely different project, through a weaponized npm package that pretended to be a normal PostCSS and Tailwind utility. Once it was on my machine, it appended its obfuscated code to config files like postcss.config.js, which is its most common target, and dropped a small batch file that ran git commit --amend and force pushed with –no-verify, even rolling my system clock back to fake the original commit time. That is exactly how malicious code ended up inside my branch, under my name, without me ever seeing a prompt or a diff. The payload’s whole purpose was to read secrets, which is why it un-ignored .env.
The part that still sits with me is the target. This was aimed at a project used by millions of people, and my own laptop was the delivery
vehicle. I always thought of my environment as separate from the code I ship. It is not. My machine is part of the supply chain too.
Burning it down instead of cleaning it up
When you are dealing with something that amends commits and force pushes behind your back, you cannot trust that you found every trace of it by reading a diff. Trying to scrub it carefully felt like exactly the kind of confidence that gets you in trouble. So I followed Albert’s suggestion, and closed the PR. And I fully removed the malware from my machine using a public write up that documents the campaign and how to clean it (https://github.com/OpenSourceMalware/PolinRider) before I wrote another line of code.
Doing it right the second time
I opened a new PR (https://github.com/woocommerce/woocommerce/pull/64247), and this time I opened it as a draft, because I wasn’t done.
Before I asked anyone anything, I tried to solve it myself. I spent about two days looking for the correct way to drop the global config and handle the sibling case properly, and I could not find it. The thing I kept missing was simple in hindsight: I never thought to split the problem into separate cases and handle each one on its own. I was looking for a single clean answer to a problem that actually had two shapes.
After those two days, I decided to ask Albert. I told him that Product Collection and Product Filters are siblings, linked only by queryId,
with no shared DOM parent, and asked what the cleanest pattern in the codebase was for this kind of per instance communication. I asked because I wanted the solution to follow the repo’s own conventions, not to invent some pattern of my own that fights the way the codebase already does things. The best solution was the one that fits the project, and Albert knows the project far better than I do.
That question was worth more than any number of speculative commits. Albert gave me two clear paths. For the case where Product Filters is a
descendant of Product Collection, the query context already flows down, so I could read forcePageReload straight from $block->context['query'] and pass it through the rest of the interactivity context. For the sibling case, I would keep my config approach but let the per instance context
take priority over the global config when it is defined.
So that is what I built. The flag now travels through real per instance context using providesContext and usesContext, with the config only as a fallback. I added the types, updated the tests, wrote the changelog, and fixed the PHPStan baseline. The fix was finally correct, not just working. It was merged.
What I took from this
On the code side, the lesson is the one I keep circling back to. Reach for the scoped, correct tool first. “It works” and “it’s correct” are two
different review gates, and a maintainer’s job is to catch the gap between them.
On security, this changed how I think about my own setup. I now assume my environment is part of what I ship. I rotated the secrets that were reachable, audited my lockfile and editor extensions for the known bad packages, and I read my own diff line by line before pushing, instead of trusting that the only changes are the ones I remember making.
And on collaboration, the strongest move in this whole story was a single well scoped question to the right person, but only after I had earned it. I spent two days trying to find the correct fix on my own before I asked. Trying first is what made the question sharp, and asking the maintainer is what kept my solution aligned with the repo’s own conventions instead of some pattern I invented that fights the codebase. The goal was always the solution that fits the project, and sometimes the fastest way to that is to ask the person who knows it best.

Leave a Reply