Looking for Trouble in My Own Tool

For background, check out these posts: Building AI Tooling that Matters + Fun with Accessibility and When I Couldn't Resolve a Bug in my Project

Most of the bugs I've written about so far found me. I'd use the tool normally, scan a site, and something would error. A few more showed up that way this round too. But partway through, I started asking myself a different question. Not "what's going to break this if I keep using it." More like: what would break this if someone were trying to break it?

That second question is the one that led to a big learning for me. But let me back up.

The one that looked like a false positive

I scanned a well-known site with my accessibility tool (link is at the end of this post) and got a contrast violation on something that looked like a button. Okay, that seems legit.

I checked the actual page. That "button" had aria-hidden="true" and tabindex="-1" on it. It wasn’t a real button, it had no use, and screen readers and keyboard users are told to skip it completely.

So was my tool wrong here?

I looked into it. Turns out axe-core, the accessibility engine doing the actual scanning in my accessibility tool, is working exactly as intended. It checks visual contrast no matter what assistive tech is told to ignore, because a sighted person without a screen reader still sees the thing.

So the finding was correct. It just needed some context a user wouldn't have.

I didn't want to bury the finding. I also didn't want to leave a reminder note up to the AI to add on its own, so I built something that checks for that hidden/decorative signal in code and attaches a caveat whenever it applies.

I'd actually tried the "remember to mention this" approach first, as an instruction to the model. It skipped it during testing. Not on purpose. Just because it was one optional field out of many, and that's all it took. That's when I stopped trusting soft instructions for anything that matters.

If it matters, compute it.

A bug hiding inside a bug

Next up was a weird rendering bug that took me a while to pin down.

My ticket text kept showing up with these nasty line breaks, and words were disappearing. I found it scanning another site, where the suggested fix mentioned wrapping something in a <main> landmark. The word "main," and everything after it, just vanished off the page.

The AI-generated ticket text was getting dropped straight into the page's HTML, so when that text happened to contain something that looked like a real tag, like <main>, the browser didn't show it as text. It tried to parse it as an actual, invisible element.

Doh. I fixed it by escaping that text everywhere it reaches the page. A literal <main> now shows up as four characters. Not as an instruction to the browser.

That same bug had a second, uglier symptom I didn't connect until later. Someone scanning a different site got a generic "Scan failed" message, even though the scan had actually worked and returned real results. Turns out that same escaping issue had mangled one ticket card badly enough that a button on it didn't exist anymore. So the code trying to attach a click handler to it threw an error. That error got caught in the wrong place and labeled a total failure, when really it was just one card.

Two fixes: the escaping, and making sure one broken card can't take the whole list down with it.

The one I had to go looking for

The first two bugs found me while I was just using the tool. This one wouldn't have. Nobody casually browsing around was going to stumble into it. It only shows up if you're actively trying to make it show up.

Here’s a quick recap on how the tool works. You give it a URL, it points a real browser at it, and scans the page. Before any of that happens, it checks that the URL isn't pointing somewhere it shouldn't, like an internal address or a local network.

But what I hadn’t considered up until this point was what happens after the page starts loading? A site can redirect a browser somewhere else entirely once it's already in motion. My validator checked the front door and then just stopped watching. So if a URL was built to redirect the scanner somewhere it shouldn't go, after that first check already passed, nothing would catch it.

This is usually the part where people assume a product manager wouldn't catch something like this, an engineer would.

I don't buy that.

You don't need to know how to write the fix to know to ask "wait, what happens after the door opens."

So, I asked, and confirmed the gap was real. Then worked out everything the fix actually needed to cover, not just the obvious internal addresses but the disguised versions too, since anyone determined enough to try this in the first place would try those next.

The fix re-checks every redirect the same way the original URL gets checked, in real time, as the page loads. I ran it against eighteen different ways someone might try to sneak a blocked address past it. A few of them were genuinely sneaky. I never would have thought to test for them if I hadn't sat down and tried to break my own validator on purpose.

Why I went looking

Here's the reason I went hunting for this instead of waiting for a site to expose it: I'd already found three bugs that came from just using the tool normally. That made me think about what wouldn't show up that way.

What only surfaces if someone's actually trying to misuse the thing, not just use it. That's a different question than "does this work." I don't think you get to skip it just because you're not the one writing the parsing logic.

Shipping something doesn't mean it's finished. It means you've found what you can find in the time you gave yourself, and you say out loud what's still unresolved. This round, that included a real gap in my own acceptance criteria. I'd rather be the one who finds it, than have someone else find it first.

Check it out at: https://a11y-triage.stephpawlowski.com

Full write-up on Github at https://github.com/stephpawlowski/accessibility-triage-tool