DEV Community

Tomasz
Tomasz

Posted on

I built a safety tool for the AWS Console, then shipped a 6-second hole in it

We've all seen the postmortem, or written it: wrong account in the tab, one wrong click.

The AWS Console's friction on destructive actions is oddly uneven. S3 makes you type the bucket name. RDS wants you to type "delete me". But EC2 Terminate instance just fires. And a security-group rule opening 0.0.0.0/0 to the world gets a small yellow warning icon — not a stop sign.

Worse, none of it is account-aware. You get exactly the same (lack of) friction in your sandbox and in production — which is no help at all when the entire problem is that you're in the wrong tab.

So I built a browser extension that adds the missing step: on a guarded account, clicking a destructive action holds the click and shows a confirmation dialog that names the account you're about to do it in.

It's free and MIT-licensed. But the interesting part isn't the idea — it's what broke.

Intercepting a click before the page sees it
The core trick is a capture-phase listener on document. Capture runs before the event reaches the target, so you can cancel it before the page's own handler ever fires:

document.addEventListener("click", onClickCapture, true); // true = capture phase

function onClickCapture(e) {
if (!guardActive || bypass) return;

const info = controlInfo(e.target);
const rule = matchRule(info);
if (!rule) return;

e.preventDefault();
e.stopImmediatePropagation();

confirmModal(rule).then(ok => {
if (!ok) return; // user cancelled — the click never happens
bypass = true;
try { info.ctrl.click(); } // replay the original click
finally { bypass = false; }
});
}
Two details make this work on a real app:

Click events are composed. They cross shadow DOM boundaries, so a single document-level listener still sees clicks on controls inside shadow roots. Modern AWS Console UI leans on web components, so without this you'd catch nothing.

You have to replay the click. Since we cancelled the original event, confirming means synthetically re-firing it past a bypass flag. That flag turned out to be where the interesting bug lived (below).

Reading the account ID from the DOM, not your cookies
The extension needs to know which account you're in. The obvious route — read the session cookie — is exactly what you should never do: it would require the cookies permission, and asking for read access to AWS session cookies is a trust non-starter for a tool people put in front of their production console.

So it scrapes the 12-digit account ID out of the page DOM instead. Permissions stay at storage plus the Console origins. Zero network requests, no backend, no telemetry.

It also fails safe: every account is treated as PRODUCTION until you explicitly mark it non-production. If detection fails, you get more friction, never less.

Four things that broke

  1. The Security Group editor lives in an iframe The 0.0.0.0/0 check silently did nothing. The rules editor renders in a dynamically created, same-origin iframe, and my content script wasn't in it. Fix:

{
"all_frames": true,
"match_origin_as_fallback": true
}
match_origin_as_fallback (Chrome 119+) is the one that matters — it injects into frames created via about:blank/blob: that inherit the origin. Then the account-ID lookup had to walk up to the parent frame, since the iframe's own DOM doesn't contain it.

  1. AWS's design system ate my modal The Console ships Cloudscape, whose global button styles bled straight into my confirmation dialog — buttons rendered with no background and invisible text. The fix is unglamorous but necessary: explicit resets on every property the host app might set.

.ga-modal-btns button {
-webkit-appearance: none; appearance: none;
background: #fff; color: #16325c;
font: 600 13px/1.2 Arial, sans-serif;
box-shadow: none; letter-spacing: normal; text-transform: none;
}
If you inject UI into someone else's app, assume every unset property is hostile. I hit the same thing on Salesforce (SLDS) and SAP (UI5).

  1. "Delete" matched everything Naive substring matching on button labels meant a rule for Delete fired on every Delete button in the entire Console. Two fixes — word-boundary matching, and scoping generic labels to a service by URL:

// "terminate" matches "Terminate instance" but not "determinate"
const re = new RegExp("(^|[^a-z])" + label + "([^a-z]|$)");

// a bare "Delete" only counts inside Lambda
{ label: "Delete", urlMatch: "/lambda/", note: "Lambda function deletion" }

  1. The security bug: my bypass window was a blind spot This is the one worth writing down.

Some Console actions re-render their menu between the confirm and the replay, so the replayed .click() landed on a detached node and did nothing. My fix was a short time window after confirming, during which clicks pass through:

// The bug
if (Date.now() < bypassUntil) return; // ~6 seconds of "let everything through"
That works. It also means that for six seconds after confirming any destructive action, every other destructive action was completely unguarded. Confirm a Lambda delete, then click Terminate on an EC2 instance three seconds later, and it just… goes.

I'd built a safety tool with a recurring six-second hole in it — and shipped it. A user testing it noticed the modal "stopped appearing sometimes" and reported it as a UI glitch. It wasn't a glitch; it was the guardrail switching itself off.

The fix is one line — scope the window to the action that was actually confirmed:

// The fix
if (Date.now() < bypassUntil && rule.label === bypassLabel) return;
Now confirming a Lambda delete only lets that action through. Everything else stays guarded.

The lesson I keep coming back to: a bypass is a security control, and it needs the same scrutiny as the thing it's bypassing. Mine was written as a UI workaround, so I never reviewed it like a security decision.

What it deliberately doesn't do
It's browser-only. It will not save you from a CLI or API fat-finger.
It's not a replacement for SCPs, MFA-delete, or deletion protection. It's click-time friction on top of those.
It doesn't touch your credentials. No cookie access, no network calls, no backend.
Defense in depth means the browser layer is a legitimate layer — it's just not the only one.

Try it
Source (MIT): https://github.com/codedigital-software/guardrail-aws
Chrome Web Store: https://chromewebstore.google.com/detail/guardrail-for-aws-console/fhmnpgipdegjkmmhepiklkjcmoeghcko
Genuinely curious about one thing: which Console actions do you most wish had a "this is PROD, are you sure?" step? The set I picked — Terminate, Delete bucket, Delete DB, Delete function, and the 0.0.0.0/0 rule check — is an informed guess, not gospel.

Top comments (0)