docs

Bambda

Match flows with a line of Lua when the field query runs out — filter History, gate Intruder payloads, and drive rule and Workflow conditions, all in Community.

When the field query can't say what you mean, write the match as code. Bambda takes a line of Lua over a flow and runs it anywhere Hugin filters or matches traffic.

It's Hugin's version of Burp's filter-by-Bambda, written in Lua rather than Java — a Burp Bambda won't paste in unchanged, but the idea carries over: when Search and the filter fields fall short, drop to code. Bambda is part of Hugin's filtering, so it's Community with no Pro gate.

Bambda comes in two match forms. A full Lua expression over a flow table cuts down History and drives the MCP tool. A one-line predicate (req.host == "target.com") matches flows inside rules, Intruder, and Workflows. A third, related form rewrites a flow on every send.

Filter flows with Lua

Write a Lua expression that returns true to keep a flow, false to drop it. It runs in three places:

The History filter bar

Prefix a query with bambda: and the rest of the line is Lua, run over every flow in the list. This is a whole-query mode — it replaces the field filter for that query rather than combining with it.

Capture rules

A Filters capture rule can use a Bambda as its condition, then keep, flag, or drop the flow at the proxy. Flagging a match is how you pre-highlight a class of traffic before you reach History.

The MCP bambda tool

Drive it from an AI agent over MCP. The tool filters up to 1,000 flows a pass, runs over full flows (headers and bodies, not just metadata), and can also transform — return a value per flow to pull every token or Set-Cookie out of History at once.

The flow table

In the History filter bar you get a flow's metadata. Headers and bodies aren't loaded there — they're empty strings and tables — so match on those through the MCP tool, which loads the full flow.

Available everywhere:

  • flow.method, flow.host, flow.path, flow.status (a number; 0 until the response lands)
  • flow.content_type, flow.content_length (response size in bytes)
  • flow.is_tls, flow.has_params, flow.latency_ms, flow.flagged, flow.tags (a list)

Full flows only (the MCP tool):

  • flow.url, flow.annotations (your notes on the flow)
  • flow.request_headers["name"], flow.response_headers["name"] — names are lowercase, nil when the header is absent
  • flow.request_body, flow.response_body (strings; opt in to bodies)

Examples

In the History filter bar, server errors with a body over 100 KB, then the unusual status codes worth a look:

bambda:return flow.status >= 500 and flow.content_length > 102400
bambda:return flow.status ~= 200 and flow.status ~= 301 and flow.status ~= 302 and flow.status ~= 304

Headers and bodies need full flows — run these through the MCP bambda tool. A POST with a JSON request body that came back 200:

local ct = flow.request_headers["content-type"] or ""
return flow.method == "POST" and ct:find("json") ~= nil and flow.status == 200

Flows to an API host carrying an Authorization header:

return flow.host:match("api%.") ~= nil
   and flow.request_headers["authorization"] ~= nil

Responses that set a cookie without HttpOnly — session cookies in reach of script:

local sc = flow.response_headers["set-cookie"]
return sc ~= nil and sc:lower():find("httponly") == nil

The editor ships presets for the common hunts — server errors, requests with parameters, Authorization present, Set-Cookie responses, no-cache responses, empty bodies, interesting status codes — and a Generate from prompt button that writes the Lua from a description. Save your own as a named preset to reuse across targets.

The Lua runs sandboxed: no os, io, require, debug, or file access, and a 100,000-instruction cap per flow stops a runaway loop. A script error drops the offending flow, not the whole filter.

Match flows with a predicate

The predicate is a one-line boolean over a flow, no return, no full Lua. It reads:

req.host  req.path  req.method  req.url  req.header(NAME)  req.body
resp.status  resp.header(NAME)  resp.body

with the operators ==, !=, contains, starts_with, ends_with, and matches (regex), joined by && and ||. Header names go bare in the parentheses and match case-insensitively; the value on the right is quoted. resp.* only resolves once the response is in — on the request side those clauses are false.

It's the condition language for matching flows beyond a field comparison:

Match and replace and intercept rules

Use a Bambda as a rule condition so an edit fires only on the flows you mean.

Intruder — Skip If Bambda

Gate each payload through a predicate (the payload lands in req.body), so Intruder skips the requests you don't want to send.

Workflows — Run Bambda

Branch a Workflow on whether the current flow matches.

Authorize

Drive an access-control check off a flow condition.

Some predicates:

resp.status == 403
req.method == "POST" && req.header(Content-Type) contains "json"
req.header(Authorization) starts_with "Bearer"
req.body matches "\d{4,}"
resp.body contains "SQL syntax"

A predicate that doesn't parse matches nothing — it fails shut rather than firing on every flow. Regex in matches runs in linear time with no catastrophic backtracking, and a pattern is capped at 4 KiB, so a bad expression can't hang the proxy.

The predicate has no >=. For a status class, write resp.status starts_with "5" or resp.status matches "^5..", not resp.status >= 500. Numeric comparison like flow.status >= 500 belongs to the Lua flow filter above, which is a different language.

Rewrite a flow on every send

The third form mutates traffic instead of matching it. Attach a Bambda to Repeater — an outbound script before the request goes out, an inbound script after the response returns — or use it as the Bambda Script edit in Match and replace.

The script gets the request as globals (method, url, host, path, headers, body) and the response (status, headers, body); reassign any of them to change the flow. A read-only env table exposes your Environment variables, so env.TOKEN weaves a saved value into a header on every send. Same sandbox, same 100,000-instruction cap.

Reach for the Lua flow filter when you're cutting History down to the flows you want. Reach for the predicate when a rule, payload, or Workflow step needs a yes/no on the current flow. Reach for a Repeater Bambda when you need to change a request every time you fire it — sign a body, refresh a nonce, inject a token.

Last updated 2026-06-17.