New in Suricata 9 and later
Precision hunting with subslice
Most Suricata rules express the presence of content in a buffer, and the existing offset and depth modifiers already pin that content to a position anchored from the start. What’s been awkward is the other direction — anchoring to the *end* of a buffer, like the final bytes of a URI — and feeding a precisely-bounded slice of a buffer into a transform. Both have traditionally meant reaching for a regular expression. The subslice transform keyword addresses that gap directly. It was requested in Redmine issue 7672 and merged to the main branch on April 29, 2026 with this PR.
What does subslice do?
subslice is a transform
that creates a window from a sticky buffer before content matching
runs.
Syntax:
subslice: offset [, nbytes] [, truncate];
The start of the window is specified
by offset; the window size is controlled by
nbytes. Both parameters accept negative values, which
count backward from the end of the buffer. Everything outside the
window is discarded before subsequent matches run.
truncate is a flag with
no associated value. It controls behavior when the window
parameters extend past the buffer boundary. Without truncate, an
out-of-bounds slice produces an empty buffer — detectable with
bsize:0. With truncate, the slice is
capped at whatever data is available.
Examples
1. Match file extension at the end of a URI
Webshells commonly use
.php extensions. The following rule creates a 4-byte
window at the end of the URI and matches against it:
alert http any any -> any any (
msg:"PHP file accessed";
http.uri; subslice: -4; content:".php"; bsize:4;
sid:1001; rev:1;)
subslice: -4 starts 4
bytes from the end of the URI. bsize:4 confirms the
buffer is exactly 4 bytes, enforcing that .php is the
final four characters rather than appearing anywhere in the path.
This is a case where the existing depth and
offset modifiers can’t help — they count from the
start of the buffer, not the end. Because the match is anchored to
the end of http.uri, it fires only on URIs that literally end in
.php; a request for /shell.php?id=1 ends in the query string, so
the last four bytes are id=1 and the rule won’t match — worth
keeping in mind, since webshell requests often carry
parameters.
2. Match file magic bytes — subslice vs. depth
PE files begin with the two-byte
magic value MZ. Here is the same detection written two
ways:
Using depth:
alert http any any -> any any (
msg:"PE file in HTTP response";
file.data; content:"MZ"; depth:2;
sid:1003; rev:1;)
Using subslice:
alert http any any -> any any (
msg:"PE file in HTTP response";
file.data; subslice: 0, 2; content:"MZ"; bsize:2;
sid:1003; rev:1;)
For this specific case,
depth:2 is simpler and familiar to any Suricata rule
writer — use it. subslice is the better choice when
the inspection window needs to feed into a transform chain. The
difference shows up the moment a transform enters the picture — say
you want to lowercase those first two bytes before matching:
alert http any any -> any any (
msg:"PE file in HTTP response, case-insensitive";
file.data; subslice: 0, 2; to_lowercase;
content:"mz"; bsize:2; sid:1003; rev:2;)
depth:2 can constrain
*where* content looks, but it can’t hand a 2-byte window to
to_lowercase (or to_sha256, or
from_base64) — only a transform can feed another
transform. That composability is what Part 2 of this series covers
in detail.
3. Skip a fixed-length header
Some C2 frameworks prepend a fixed-length header or nonce before the payload. PowerShell Empire, for instance, frames its agent traffic with a fixed 20-byte routing header (a 4-byte RC4 IV followed by a 16-byte encrypted routing block) ahead of the agent data. When a protocol consistently uses a fixed-size header and the bytes that follow are inspectable, subslice positions the inspection window past the header:
alert http $HOME_NET any -> $EXTERNAL_NET any (
msg:"C2 callback pattern"; flow:to_server;
file.data; subslice: 20; content:"heartbeat";
sid:1004;)
subslice: 20 skips the 20-byte header
so content inspects only what follows it. Two details
matter here. The rule is outbound — a callback is an internal host
beaconing to an external server, hence $HOME_NET ->
$EXTERNAL_NET with flow:to_server so
file_data resolves to the request body the agent
sends. And the buffer is file_data, the HTTP body, not
the raw TCP payload. Transforms attach only to sticky buffers, so
there is no pkt_data; subslice form; the “When
subslice isn’t the right tool” section below returns to this.
4. Detect missing or too-short User-Agents
subslice can detect the
absence of sufficient data. To flag User-Agents shorter than 8
bytes:
alert http any any -> any any (
msg:"Suspiciously short User-Agent";
http.user_agent; subslice: 0, 8;
bsize:0;
sid:1006; rev:1;)
Without truncate, a
buffer shorter than 8 bytes produces an empty result.
bsize:0 matches that empty result. It’s a quick way to
hunt for minimal or skeleton HTTP stacks by expressing a minimum
buffer length requirement directly in the rule.
A note on truncate
With truncate, the slice
is capped at the available data in the buffer. Use
truncate when matching a prefix or suffix regardless
of overall field length. Omit it when buffer length is itself a
signal worth detecting.
When subslice isn’t the right tool
subslice requires fixed
offsets. If the target pattern doesn’t appear at a predictable
position — a value embedded anywhere in a variable-length field,
for example — a plain content match is simpler and
correct. Using subslice in that case would require a
separate rule for each possible offset.
For positional matching without
transforms, the existing content modifiers (offset,
depth, within, distance) are
appropriate and familiar. subslice is most useful when
negative offsets are needed or when the inspection window feeds
into a transform chain — the upcoming Part 2 covers that in
depth.
For length-prefixed binary protocols,
subslice alone is insufficient. If the inspection
position depends on a length value embedded in the packet,
byte_extract and byte_math are needed to
compute that offset — subslice parameters must be
constants. When detection requires matching at two different
positions in the same buffer, two separate rules are necessary;
subslice creates one window per transform chain.
Try it out!
The examples from this post are in the following rule file along with the pcap and the script used to create the pcap. Remember, this transform is only in Suricata’s main branch
- Subslice rule file (add -S /path/to/these.rules): subslice rules
- Pcap file to use with the subslice rule file (add -r /path/to/this.pcap): subslice pcap file
- Python program that created the pcap file: pcap generator script
Wrapping up
subslice extends
Suricata’s detection language with positional matching. The
existing content modifiers handle many positional cases, but
negative offsets — and feeding a precisely-bounded buffer into a
transform chain — required different solutions before this keyword.
With subslice, a rule can express “this content
appears at this position” as a single, composable step.
Part 2 of this series gets into how
transforms interact with Suricata’s prefilter and rule evaluation
pipeline, how to chain subslice with other transforms,
and where the keyword is headed with planned support for dynamic
offsets via byte_extract.
The post Suricata keyword highlight: subslice appeared first on Suricata.

