Security news weekly round-up – 27th March 2026

While cybersecurity defenders are looking for innovative ways to keep Internet users safe, cybercriminals are doing the opposite — to hurt users by stealing their money or information that can lead to theft or other things that are valuable to the user. It’s upon me and you to always know the threat out there and act accordingly.

Hackers Use Fake Resumes to Steal Enterprise Credentials and Deploy Crypto Miner

It’s a phishing attack. To complicate issues, if you fall for it, it takes around 25 seconds from script execution to credential exfiltration.

Here is what’s going on:

The initial dropper file is a Visual Basic Script (VBScript) that, upon opening, displays a bogus French-language error message, fooling message recipients into thinking that the file is corrupted.

…the heavily obfuscated script runs a series of checks to evade sandboxes and enters into a persistent User Account Control (UAC) loop that prompts users to run it with administrator privileges.

As soon as the dropper obtains administrative privileges, it wastes no time disabling security controls and covering up its tracks

Convicted spyware chief hints that Greece’s government was behind dozens of phone hacks

While reading the article, one thing is clear: someone hacked the phones of government officials and journalists. The spyware chief was sentenced to eight years in prison and now he claims he will not be a “scapegoat.”

From the article:

Several senior officials in the Greek government, including the head of Greece’s national intelligence agency and a senior aide to the Prime Minister Kyriakos Mitsotakis, resigned in the wake of revelations that several journalists’ phones had been hacked.

No government officials have been convicted in connection with the surveillance, and critics have accused the Mitsotakis government of a cover-up.

WebRTC Skimmer Bypasses CSP to Steal Payment Data from E-Commerce Sites

At the time of writing, Adobe has patched the vulnerability that allowed this to happen, but it appears that the patch is yet to reach production websites.

The following is how the skimmer works:

The skimmer is designed as a self-executing script that establishes a WebRTC peer connection to a hard-coded IP address (“202.181.177[.]177”) over UDP port 3479 and retrieves JavaScript code that’s subsequently injected into the web page for stealing payment information.

The use of WebRTC marks a significant evolution in skimmer attacks, as it bypasses Content Security Policy (CSP) directives.

Apple made strides with iOS 26 security, but leaked hacking tools still leave millions exposed to spyware attacks

The article title says it all. If you’re not in the loop in the past few weeks, let me update you. The hacking tools in question are DarkSword and Coruna. The former was leaked on GitHub, making it easy for anyone to launch attacks on older iOS users.

From the article:

The discovery of Coruna and DarkSword suggest that memory-based attacks could continue to plague users of older iPhones and iPads that lag behind the newer, more memory-safe models.

Experts working for iVerify and Lookout, two cybersecurity companies that have a commercial stake in selling security products for mobile devices, say Coruna and DarkSword may also challenge the long-held assumption that iPhone hacks are rare.

Credits

Cover photo by Debby Hudson on Unsplash.

That’s it for this week, and I’ll see you next time.

6 Async JavaScript Patterns That Prevent Partial Failures in Production

Most async code works fine until one step fails halfway through a workflow. Then you get double charges, missing data, or silent corruption.
These patterns fix that.

1. Replace Sequential Awaits With Compensated Steps

Sequential code looks clean but breaks on partial failure.

Before

async function processOrder(orderId: string) {
  const order = await fetchOrder(orderId)
  const payment = await chargeCustomer(order.customerId, order.total)
  const shipment = await createShipment(order.items, order.address)

  return { order, payment, shipment }
}

If shipment fails, payment is already done. No rollback.

After

async function processOrder(orderId: string) {
  const order = await fetchOrder(orderId)

  let payment
  try {
    payment = await chargeCustomer(order.customerId, order.total)
  } catch {
    throw new Error('PAYMENT_FAILED')
  }

  try {
    const shipment = await createShipment(order.items, order.address)
    return { order, payment, shipment }
  } catch {
    await refundPayment(payment.id).catch(() => {
      logger.fatal('REFUND FAILED', { orderId })
    })

    throw new Error('SHIPMENT_FAILED')
  }
}

You explicitly define rollback logic. This is what real systems do.

2. Start Independent Promises Early

Most developers accidentally serialize independent work.

Before

async function loadData(userId: string) {
  const user = await fetchUser(userId)
  const analytics = await fetchAnalytics(userId)
  return { user, analytics }
}

Total time = sum of both calls.

After

async function loadData(userId: string) {
  const analyticsPromise = fetchAnalytics(userId)

  const user = await fetchUser(userId)
  const analytics = await analyticsPromise

  return { user, analytics }
}

You save latency without changing logic. This pattern shows up in senior interviews .

3. Guard Multi-Call Flows With Promise.allSettled

Dashboards should not fail entirely because one API is down.

Before

const [users, orders, stats] = await Promise.all([
  fetchUsers(),
  fetchOrders(),
  fetchStats()
])

One failure kills everything.

After

const results = await Promise.allSettled([
  fetchUsers(),
  fetchOrders(),
  fetchStats()
])

const [users, orders, stats] = results.map(r =>
  r.status === 'fulfilled' ? r.value : null
)

Now partial data renders. Failures become observable, not catastrophic.

4. Retry Only Transient Errors With Backoff

Retrying everything is worse than not retrying.

Before

await fetch('/api/payment')

One network hiccup breaks the flow.

After

async function retry(fn, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn()
    } catch (e) {
      if (e.status && e.status < 500) throw e
      await new Promise(r => setTimeout(r, 2 ** i * 100))
    }
  }
  throw new Error('FAILED_AFTER_RETRIES')
}

await retry(() => fetch('/api/payment'))

You retry only when it makes sense. This avoids hammering APIs.

This becomes critical when dealing with flaky external systems like described in the Node.js memory leak debugging scenarios where retries amplify system pressure if done wrong.

5. Cancel Stale Requests With AbortController

Race conditions are one of the most common production bugs.

Before

useEffect(() => {
  fetch(`/api/search?q=${query}`)
    .then(r => r.json())
    .then(setResults)
}, [query])

Old responses overwrite new ones.

After

useEffect(() => {
  const controller = new AbortController()

  fetch(`/api/search?q=${query}`, {
    signal: controller.signal
  })
    .then(r => r.json())
    .then(setResults)
    .catch(err => {
      if (err.name !== 'AbortError') throw err
    })

  return () => controller.abort()
}, [query])

Now only the latest request wins. No stale UI.

6. Limit Concurrency Instead of Flooding APIs

Firing 100 requests at once will get you rate-limited or banned.

Before

await Promise.all(ids.map(id => fetchItem(id)))

Unbounded concurrency.

After

async function limit(tasks, concurrency) {
  const results = []
  let i = 0

  async function worker() {
    while (i < tasks.length) {
      const current = i++
      results[current] = await tasks[current]()
    }
  }

  await Promise.all(
    Array.from({ length: concurrency }, worker)
  )

  return results
}

const tasks = ids.map(id => () => fetchItem(id))
await limit(tasks, 5)

You control throughput. Systems stay stable under load.

Closing

Take one of your existing async flows and add compensation or cancellation today. That alone removes an entire class of production bugs.
If your code assumes the happy path, it is already broken.

When /pair approve Bypasses the Scope Guard

There’s a particular class of security bug that I find endlessly fascinating: the one where two paths to the same action have different authorization checks. One path is locked down tight. The other… someone forgot.

#55995 is exactly that. CVSS 9.9. Critical. And the fix is 8 lines of code.

The Setup

OpenClaw’s device pairing system lets you connect phones, tablets, and other “nodes” to your gateway. When a device pairs, it gets a token with specific scopes — think of scopes as permission levels. operator.pairing lets you manage device connections. operator.admin lets you do… everything.

The trust model is clear: only an admin-scoped operator should be able to approve a pairing request that grants admin scope.

This is enforced in the core approveDevicePairing function. It accepts an optional callerScopes parameter. When present, it checks: does this caller have sufficient scope? If not, rejection. Good design. There are tests for it.

The Bypass

The device-pair plugin exposes a /pair approve slash command:

if (action === "approve") {
  // Coarse check: does the caller have *any* pairing-related scope?
  if (gatewayClientScopes &&
      !gatewayClientScopes.includes("operator.pairing") &&
      !gatewayClientScopes.includes("operator.admin")) {
    return { text: "Requires operator.pairing" };
  }
  // Approve WITHOUT forwarding callerScopes
  const approved = await approveDevicePairing(pending.requestId);
}

The slash command checks “do you have some pairing scope?” but calls approveDevicePairing() without passing callerScopes. The core function only enforces the scope guard when callerScopes is present.

So an operator with just operator.pairing can /pair approve a pending request asking for operator.admin. Privilege escalation complete.

Why This Pattern Keeps Happening

This is the dual-path authorization gap:

Path A (RPC/API): Carefully designed, passes all context for authorization.

Path B (convenience layer): Built later as a friendly wrapper. Calls the same core function but forgets to thread through one critical parameter.

The core function made callerScopes optional. That optionality became the vulnerability.

  • Core function assumes: “if callerScopes is missing, the caller is trusted”
  • Plugin assumes: “if the coarse scope check passes, we’re good”
  • Neither assumption is wrong in isolation. Together, they’re a CVSS 9.9.

The Fix

Eight lines. Pass callerScopes through. Plus a test.

Lessons for Agent Builders

  1. Optional security parameters are dangerous. If callerScopes were required, the plugin author would have been forced to think about what to pass.

  2. Every path to a privileged action needs the same checks. RPC, slash command, API endpoint, cron handler — test each independently.

  3. Convenience layers are where auth bugs hide. Core infra built solid authorization. The plugin built a nice UX wrapper. Nobody checked the wrapper preserved the security properties.

  4. Pairing and provisioning are trust-granting operations. They deserve the same scrutiny as authentication.

Found via #55995. Fix in #55996.

I Was Hand-Writing Every AI Tool. Then I Discovered MCP Servers.

What tool calling and MCP actually mean, and how they fit together when you’re building real AI products.

I’ve been building Pulse, a voice AI co-pilot for engineering work that talks to Jira and GitHub. The idea is simple: speak a command, Claude figures out what to do, your project management tools respond.

To make it work, I had to give Claude the ability to interact with Jira and GitHub. So I did what most people do when they start building with LLMs: I wrote the tools by hand.

tools: [
  { name: "create_jira_ticket", description: ""...", input_schema: { ... } },"
  { name: "get_jira_issue", description: ""...", input_schema: { ... } },"
  { name: "update_jira_status", description: ""...", input_schema: { ... } },"
]

Three tools. Done. It worked fine.

Then I learned what an MCP server actually is, and I realised I had been building with a teaspoon when a fire hose was sitting right there.

First, what is tool calling?

When you build an LLM application, the model lives in a box. It can think, reason, and generate text, but it cannot do anything in the real world on its own.

Tool calling is how you fix that. You define a set of functions and tell the model they exist. When the model decides one is needed, it calls it with the right arguments. Your code executes the function and passes the result back to the model.

User gives instruction
      ↓
Claude decides which tool to call
      ↓
Your code executes it
      ↓
Claude gets the result and responds

It’s powerful. But you write every tool yourself. You define the schema, you maintain it, you add new ones when you need them.

So what is an MCP server?

MCP stands for Model Context Protocol. The simplest way I can explain it:

Tool calling is giving Claude a telephone. MCP is giving Claude a universal remote control.

The telephone analogy works because with hand-rolled tools, you’re wiring up each number yourself. You decide what Claude can call, you write the definition, you maintain it forever.

An MCP server is a running process that speaks a standard protocol. Claude connects to it and asks: “what tools do you have?” The server responds with a full list. Claude now knows everything it can do without you having written a single tool definition.

More concretely:

Tool Calling MCP Server
What it is Functions you define manually A running server Claude connects to
Setup You write each function and schema Server exposes its tools automatically
Reusability Tied to your app Any AI that speaks MCP can use it
Maintenance Yours forever The server owner’s problem

Jira has an MCP server. GitHub has an MCP server. Instead of my 3 hand-rolled tools, I can point Pulse at both servers and Claude automatically gets access to the full API surface of each product.

Sprints. PRs. Worklogs. Reviews. Branches. Comments. Issue history.

Same voice interface. Dramatically larger capability surface.

The part that actually blew my mind

Claude can reason across multiple MCP servers simultaneously.

So instead of three isolated tool calls, you can say:

“Find all Jira tickets marked Done this sprint, check if the linked PR was actually merged, and flag any that say Done but the PR is still open.”

That used to be a custom script someone had to write, test, and maintain. With MCP, it’s a voice command.

And when Jira ships new API features? The MCP server updates. Claude automatically has access. You change nothing.

The broader point

Most people are still thinking about AI as a chatbot. You ask it something, it answers.

What I’ve been building toward is composing intelligence over systems. Claude isn’t just answering questions. It’s reasoning across your Jira board, your GitHub history, and your internal data, then taking action.

The tools that make that possible are not complicated individually. What takes work is understanding how they fit together and when to use each one.

That’s what I’m figuring out in public, one project at a time.

I’m Rue, a Full Stack and AI Engineer building Pulse and writing about what I learn along the way. Follow along on Instagram at @rue.on.ai or connect with me on LinkedIn.

Wrapping Up Bloom After: Week 4 Polish & Full Sprint Retro

We finally made it to Week 4! This last week of the sprint was all about crossing the t’s, dotting the i’s and making sure the app actually feels good to use.

Before I get into the bugs I squashed this week, let’s zoom out and talk about what my team and I spent the last month building.

What is Bloom After?

Dealing with postpartum depression (PPD) can feel incredibly lonely. While there is plenty of medical advice online, finding a single and calming place with helpful resources, verified specialists and a community of moms who actually get it is really hard. That’s why we built Bloom After. It’s a safe digital space for mothers facing maternal mental health challenges, featuring an educational hub, a clinic finder, and a story library where moms can read and share their journeys. We designed the entire platform to be calming, supportive and super easy to use for someone who might already be feeling overwhelmed.

What I Shipped This Week

The heavy lifting for the Stories feature and the Admin dashboard was finished last week by my team. So, my main goal for Week 4 was to polish the app, test it, and make sure everything flowed perfectly. Here is what I did:

  • Revamped the Lifestyle & Crisis Pages: I completely refined the UI and functionality for the Lifestyle and Crisis landing pages. For the Crisis page, I made sure the 24/7 helplines were pinned to the very top and swapped out a clunky separate page for a fast and instant-loading modal so moms can get help immediately.

  • End-to-End Testing: I put on my QA hat (which almost didn’t fit) and clicked through the entire app from start to finish, acting like a real user to see what would break.

  • Bug Squashing & UI Polish: I spent hours fixing alignment issues, spacing and mobile glitches (like a stubborn bug on 360px mobile screens!). I went through several pages, including the Admin dashboard, to fix bugs and make sure the design looked premium and consistent everywhere.

Key Technical Lessons from the Sprint

  1. CSS Mobile Quirks are Real: I learned that mobile browsers handle screen heights weirdly. Using dvh (dynamic viewport height) instead of standard vh is a lifesaver so your modals don’t get hidden behind the mobile address bar.

  2. The Devil is in the Details: Polishing UI takes just as much time as building the feature in the first place. A tiny 1px gap or a misaligned button can ruin the whole vibe of a page.

  3. E2E Testing is Eye-Opening: You think your code works perfectly until you actually try to use it. Testing the full flow caught so many tiny bugs and bad user experiences that I would have completely missed if I only looked at the code.

Full-Sprint Retrospective

Looking back at the whole month of building Bloom After:

  • What went well: We stuck to our vision. We wanted to build a platform that felt deeply empathetic and safe, and we never compromised on that UX, even when it meant doing tedious CSS work.

  • What I would do differently: I’d set up a better modular folder structure from Day 1. Moving from a messy flat folder system to a clean and organized one late in the project meant rewriting a ton of file paths.

  • What surprised me: How much complex code goes into making things look “simple.” Creating a clean and calming interface actually requires a lot of behind-the-scenes work.

What’s Next for Me?

Now that this sprint is over, I’m shifting my focus to leveling up my core engineering skills. I really want to master the fundamentals, so I’m going to dive deep into advanced JavaScript and TypeScript. I also plan to tackle Data Structures and Algorithms (DSA) to sharpen my problem-solving abilities. Instead of just getting stuck in “tutorial hell,” my main goal is to build out a few solid projects from scratch using JS and TS to put all of these concepts into actual practice.

Team Shoutouts

Building this isn’t a one-person job. A massive thank you to the amazing people building this with me:

  • Chijioke Uzodinma (Backend Lead):Read his Week 4 article here.

  • Nanji Lakan (Product Lead): Read her Week 4 article here.

  • Agugua Genevieve (Design Lead): Read her Week 4 article here.

  • Prisca Onyemaechi (Lead Maintainer): Read her Week 4 article here.

  • Adejola Esther (Frontend Engineer): Read her Week 2 article here.

Visit our live site.
Check out our Github Repo.

I Built a Skill So Claude Automatically Routes Tasks to Free-Tier AI Providers

I Built a Skill So Claude Automatically Routes Tasks to Free-Tier AI Providers

Here’s a problem I kept running into: I have free-tier access to Groq, OpenAI, Gemini, and MiniMax — but managing them manually is painful. Wrong tool for the job, accidentally burning through monthly limits, no visibility into what’s been used.

I built agent-hub to fix this. It’s a Claude Code skill that makes Claude the orchestrator — every task is automatically classified, routed to the best provider, tracked against free limits, and shown in a live status bar.

How it works

Claude classifies every incoming message into a task type and routes it:

Task Type Signals Provider
code write/fix/debug/refactor Codex (gpt-4o-mini)
research explain/summarize/compare Gemini (gemini-2.0-flash)
creative story/dialogue/narrative MiniMax
fast yes/no, quick lookups Groq
general everything else Groq

Classification happens before calling any API — router.py checks token budgets in usage.json, picks the best available provider, calls its API, and returns the response.

Auto-fallback: when a provider drops below 10% of its free tier, traffic automatically shifts to its fallback (Groq→Gemini, Codex→Groq, Gemini→MiniMax, MiniMax→Gemini).

Hard stop: if both primary and fallback are exhausted, the router surfaces it to the user and stops. No silent failures.

The status bar

Every response starts with a live usage bar:

[GROQ ●] Groq: 1,240/14,400 · Codex: 77/500 · Gemini: 108K/1M · MiniMax: 220K/1M
  • ● green — above 50% remaining
  • ● yellow — 10–50% remaining
  • ● red — below 10% (fallback active)
  • ○ gray — exhausted

The active provider appears first in brackets. All four counts are always shown.

Free tier limits

Provider Model Limit
Groq llama-3.3-70b-versatile 14,400 req/day
Codex gpt-4o-mini 500 req/month
Gemini gemini-2.0-flash 1M tokens/day
MiniMax abab6.5s-chat 1M tokens/month

Install

SKILL_DIR="$HOME/.claude/plugins/cache/claude-plugins-official/superpowers/5.0.5/skills/agent-hub"
mkdir -p "$SKILL_DIR/tests"
curl -o "$SKILL_DIR/router.py" https://raw.githubusercontent.com/LakshmiSravyaVedantham/agent-hub/main/router.py
curl -o "$SKILL_DIR/SKILL.md" https://raw.githubusercontent.com/LakshmiSravyaVedantham/agent-hub/main/SKILL.md

Then set your keys:

ROUTER="$SKILL_DIR/router.py"
python3 $ROUTER set-key groq gsk_...
python3 $ROUTER set-key codex sk-...
python3 $ROUTER set-key gemini AIza...
python3 $ROUTER set-key minimax ...
python3 $ROUTER set-key minimax-group-id ...

Keys are stored in ~/.claude/agent-hub/.env with chmod 600 — never in code or skill files.

Usage

In any Claude Code session:

superpowers:agent-hub

Claude validates your keys, shows the initial token bar, and from that point every message is classified and routed automatically. You don’t think about providers — Claude does.

Or call router.py directly:

python3 $ROUTER route "explain how transformers work" --type research
python3 $ROUTER status
python3 $ROUTER reset groq

Under the hood

router.py is ~530 lines of Python 3.8+, two dependencies (requests, python-dotenv), 61 tests. Auto-reset logic zeros daily counters at UTC midnight and monthly counters on the 1st. Atomic writes to usage.json via .tmp + rename — no partial writes.

What’s next

  • Three-agent coordination — tested with one session, curious how routing behaves when multiple agents compete for the same provider budget
  • Claude Code hooks integration — auto-register sessions via hooks so the status bar appears without manually invoking the skill
  • File section locks — paired with agent-comms for per-file, per-section coordination

GitHub: LakshmiSravyaVedantham/agent-hub — Python 3.8+, MIT license.