Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
4ddbe7f
feat(ping): bootstrap ping builtin implementation
AlexandreYang Mar 17, 2026
4a44531
feat(ping): implement ping builtin using pro-bing
AlexandreYang Mar 17, 2026
e90fff2
[iter 1] Address review comments: gofmt, LICENSE, test scenarios, DNS…
AlexandreYang Mar 17, 2026
4f909a7
[iter 2] Fix allowedsymbols, gofmt, and fuzz filter
AlexandreYang Mar 17, 2026
e9d5931
[iter 2] Fix fuzz filter: also exclude single quote, backslash, and s…
AlexandreYang Mar 17, 2026
8afa782
[iter 2] Fix FuzzPingFlags: use allowlist instead of denylist for she…
AlexandreYang Mar 17, 2026
0d5ea98
[iter 2] Fix FuzzPingHostname: use allowlist for safe hostname charac…
AlexandreYang Mar 17, 2026
12e94b5
[iter 2] Fix DNS context-awareness and -4/-6 mutual exclusion in ping
AlexandreYang Mar 18, 2026
7242f5e
[iter 3] Fix DNS family selection: use net.ResolveIPAddr with ip4/ip6/ip
AlexandreYang Mar 18, 2026
23e7908
[iter 5] Fix goroutine leak: use net.DefaultResolver.LookupIPAddr for…
AlexandreYang Mar 18, 2026
611a19d
[iter 6] Fix timeout formula: use count*wait + (count-1)*interval
AlexandreYang Mar 18, 2026
0607659
[iter 8] Reject broadcast/multicast IPs after address resolution
AlexandreYang Mar 18, 2026
1ffdb32
[iter 10] Fix Windows WSAEPROTONOSUPPORT fallback and IPv4-prefer for…
AlexandreYang Mar 18, 2026
07b71da
[iter 11] Address P3 review findings: fuzz seed, exact stderr, multic…
AlexandreYang Mar 18, 2026
99399d7
[iter 11] Fix pro-bing Timeout formula: use last-probe deadline
AlexandreYang Mar 18, 2026
ac7eda3
[iter 12] Address P3 review findings: constants, flag docs, payload size
AlexandreYang Mar 18, 2026
478add3
[iter 12] Fix isPermissionErr: add EPROTONOSUPPORT for privileged fal…
AlexandreYang Mar 18, 2026
826b09e
[iter 13] Address review findings: comments, multicast msg, quiet sce…
AlexandreYang Mar 18, 2026
d3b1b4c
[iter 13] Fix FuzzPingHostname: reduce context timeout 3s→1s to preve…
AlexandreYang Mar 18, 2026
55323bd
[iter 14] Address review: add -h to synopsis, add ip6-flag-on-ipv4-ho…
AlexandreYang Mar 18, 2026
c2ecac6
[iter 15] Address review: clarify comments, add -I/-p/-R rejection sc…
AlexandreYang Mar 18, 2026
2397dc4
[iter 15] Fix P1: block directed broadcast addresses (pro-bing auto-e…
AlexandreYang Mar 18, 2026
4210d91
[iter 16] Address review: clarify partial-stats and IPv6 zone-ID comm…
AlexandreYang Mar 18, 2026
086baf3
[iter 16] Address codex P1s: document non-/24 broadcast limitation, a…
AlexandreYang Mar 18, 2026
16f17d9
[iter 18] Fix godoc -i range comment; move size_flag_rejected to errors/
AlexandreYang Mar 18, 2026
4662dea
[iter 19] Fix pentest test: quote $(id) so shell passes it literally …
AlexandreYang Mar 18, 2026
cbbd270
[iter 20] Add -4 with IPv6 literal scenario test (symmetric with exis…
AlexandreYang Mar 18, 2026
0b81c2f
[iter 21] Block unspecified 0.0.0.0/:: destination; add fuzz .gitignore
AlexandreYang Mar 18, 2026
cafd2e6
[iter 22] Warn when -c/-W/-i values are clamped to safety limits
AlexandreYang Mar 18, 2026
3881174
[iter 23] Add scenario tests for clamping warnings and PING header; u…
AlexandreYang Mar 18, 2026
b4cb062
[iter 23b] Fix CI: replace cross-platform scenario with Go pentest te…
AlexandreYang Mar 18, 2026
05321fb
[iter 24] Add -W clamping warning scenario; annotate stderr_contains …
AlexandreYang Mar 18, 2026
bc6cb76
[iter 26] Add -- separator to TestPingPentestLongHostname for consist…
AlexandreYang Mar 18, 2026
10b3aa8
[iter 27] Add scenario test for IPv6 unspecified :: rejection
AlexandreYang Mar 18, 2026
1b3c8bd
[iter 29] Accept integer/float seconds for -W and -i flags
AlexandreYang Mar 18, 2026
831479f
[iter 29] Fix allowlist and add Inf/NaN guard in parsePingDuration
AlexandreYang Mar 18, 2026
05d7863
[iter 29] Guard parsePingDuration against overflow, Inf, NaN
AlexandreYang Mar 18, 2026
193c847
chore: increase review-fix-loop max iterations from 30 to 50
AlexandreYang Mar 18, 2026
cf3ffa2
[iter 30] Add scenario for -c below minimum (0) clamping warning
AlexandreYang Mar 18, 2026
232c4d3
[iter 31] Always print statistics, add test for negative Go duration …
AlexandreYang Mar 18, 2026
838b326
Add platform compatibility docs to ping package comment
AlexandreYang Mar 18, 2026
a753d28
[iter 31] Reject negative Go duration literals in parsePingDuration
AlexandreYang Mar 18, 2026
5266018
[iter 32] Add scenario tests for invalid -W and -i duration values
AlexandreYang Mar 18, 2026
9787106
[iter 32] Warn when 120s total-run-time cap is applied; add negative …
AlexandreYang Mar 18, 2026
d50a6cb
[iter 33] Add scenario test for 120s total-run-time cap warning
AlexandreYang Mar 18, 2026
a780cb4
[iter 34] Clarify quiet_flag scenario description
AlexandreYang Mar 18, 2026
861a558
[iter 35] Clarify .255 heuristic comment; add wait below-min clamp sc…
AlexandreYang Mar 18, 2026
0aa523c
[iter 36] Extract pingGracePeriod constant (5s deadline grace)
AlexandreYang Mar 18, 2026
daf4eb7
[iter 37] Fix en-dash in warnings, add context comment, explain fuzz …
AlexandreYang Mar 18, 2026
f02da46
[iter 38] Add scenario documenting .255 heuristic false-positive on w…
AlexandreYang Mar 18, 2026
b441173
[iter 39] Remove .gitignore that excluded ping fuzz corpus from git
AlexandreYang Mar 18, 2026
e972d7a
[iter 40] Fix inaccurate fuzz.yml comment: helpers are scoped by dire…
AlexandreYang Mar 18, 2026
bf68474
[iter 41] Add minCount constant, use it in clampInt and warning; docu…
AlexandreYang Mar 18, 2026
34e6c7b
[iter 42] Revert unrelated SKILL.md change (max-iterations 30→50) fro…
AlexandreYang Mar 18, 2026
6201a2d
[iter 42] Use family-specific DNS lookup for -4/-6 hostname resolution
AlexandreYang Mar 18, 2026
b6effdb
[iter 43] Clarify deadline floor comment and isPermissionErr string f…
AlexandreYang Mar 18, 2026
70d883b
[iter 43] Document IPv4-mapped IPv6 limitation in family-selection co…
AlexandreYang Mar 18, 2026
1db555b
[iter 44] Expand help Note to list all 6 blocked flags
AlexandreYang Mar 18, 2026
d642114
[iter 44] Fix zone preservation for scoped IPv6 literals (fe80::1%eth0)
AlexandreYang Mar 18, 2026
f82a149
[iter 45] Address P2/P3 findings: stdout assertions, scoped IP rename…
AlexandreYang Mar 18, 2026
cb1343d
[iter 46] Add net.ParseIP and strings.IndexByte to allowedsymbols pin…
AlexandreYang Mar 18, 2026
3e7a22c
[iter 48] Add maxTotalTimeout named constant for 120s hard cap
AlexandreYang Mar 18, 2026
70963f3
[iter 49] Use maxTotalTimeout.Seconds() in warning message to avoid h…
AlexandreYang Mar 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ jobs:
- pkg: ./builtins/tests/ss/
name: ss
corpus_path: builtins/tests/ss
- pkg: ./builtins/ping/
name: ping
Comment thread
AlexandreYang marked this conversation as resolved.
# ping fuzz tests live in builtins/ping/ rather than builtins/tests/ping/
# because they share test helper functions (runScriptCtx, etc.) defined in
# ping_test.go. Go test helpers are only in scope within the same directory,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Fuzz tests co-located with source rather than in builtins/tests/ping/

All other fuzz targets live under builtins/tests/<cmd>/ (head, cat, wc, tail, grep, cut, echo, uniq, strings_cmd, testcmd, ls, ip, ss). The ping fuzz tests live directly in builtins/ping/ alongside the production source.

The inline comment in the diff explains why — shared test helper functions (runScriptCtx etc.) defined in ping_test.go are only in scope within the same directory. This is a valid technical reason and the comment documents it clearly.

This is noted for awareness only; no action required if the co-location is intentional.

# so both files must reside in builtins/ping/.
corpus_path: builtins/ping
Comment thread
AlexandreYang marked this conversation as resolved.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fuzz corpus cache path inconsistency with other builtins

Every other builtin in this matrix uses builtins/tests/<name> as both the pkg path and corpus_path — because the fuzz tests live under builtins/tests/<name>/. The ping entry uses builtins/ping because the fuzz test was co-located with the implementation.

This is functionally correct (the cache will store/restore builtins/ping/testdata/fuzz/ as expected), but it breaks the pattern established by all other builtins and may cause confusion when someone adds a new builtin and looks at the matrix for guidance.

Consider either:

  1. Moving ping_fuzz_test.go (and builtin_ping_pentest_test.go) to builtins/tests/ping/ and updating pkg/corpus_path to match, or
  2. Adding a brief comment in the matrix entry explaining why ping deviates.

This does not block merge.

- pkg: ./interp/tests/
name: interp
corpus_path: interp/tests
Expand Down
4 changes: 4 additions & 0 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ gopkg.in/yaml.v3,https://github.com/go-yaml/yaml,MIT AND Apache-2.0,"Copyright (
mvdan.cc/sh/v3,https://github.com/mvdan/sh,BSD-3-Clause,"Copyright (c) 2016, Daniel Marti"
github.com/davecgh/go-spew,https://github.com/davecgh/go-spew,ISC,Copyright (c) 2012-2016 Dave Collins
github.com/inconshreveable/mousetrap,https://github.com/inconshreveable/mousetrap,Apache-2.0,Copyright 2014 Alan Shreve
github.com/google/uuid,https://github.com/google/uuid,BSD-3-Clause,Copyright (c) 2018 Google Inc.
github.com/pmezard/go-difflib,https://github.com/pmezard/go-difflib,BSD-3-Clause,"Copyright (c) 2013, Patrick Mezard"
github.com/prometheus-community/pro-bing,https://github.com/prometheus-community/pro-bing,MIT,"Copyright 2022 The Prometheus Authors; Copyright 2016 Cameron Sparr and contributors"
github.com/spf13/cobra,https://github.com/spf13/cobra,Apache-2.0,Copyright 2013-2023 The Cobra Authors
github.com/spf13/pflag,https://github.com/spf13/pflag,BSD-3-Clause,"Copyright (c) 2012 Alex Ogier, The Go Authors"
golang.org/x/net,https://github.com/golang/net,BSD-3-Clause,Copyright (c) 2009 The Go Authors. All rights reserved.
golang.org/x/sync,https://github.com/golang/sync,BSD-3-Clause,Copyright (c) 2009 The Go Authors. All rights reserved.
golang.org/x/sys,https://github.com/golang/sys,BSD-3-Clause,Copyright (c) 2009 The Go Authors. All rights reserved.
1 change: 1 addition & 0 deletions SHELL_FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Blocked features are rejected before execution with exit code 2.
- ✅ `sort [-rnubfds] [-k KEYDEF] [-t SEP] [-c|-C] [FILE]...` — sort lines of text files; `-o`, `--compress-program`, and `-T` are rejected (filesystem write / exec)
- ✅ `ss [-tuaxlans4689Hoehs] [OPTION]...` — display network socket statistics; reads kernel socket state directly (Linux: `/proc/net/`; macOS: sysctl; Windows: iphlpapi.dll); `-F`/`--filter` (GTFOBins file-read), `-p`/`--processes` (PID disclosure), `-K`/`--kill`, `-E`/`--events`, and `-N`/`--net` are rejected
- ✅ `ls [-1aAdFhlpRrSt] [--offset N] [--limit N] [FILE]...` — list directory contents; `--offset`/`--limit` are non-standard pagination flags (single-directory only, silently ignored with `-R` or multiple arguments, capped at 1,000 entries per call); offset operates on filesystem order (not sorted order) for O(n) memory
- ✅ `ping [-c N] [-W DURATION] [-i DURATION] [-q] [-4|-6] [-h] HOST` — send ICMP echo requests to a network host and report round-trip statistics; `-f` (flood), `-b` (broadcast), `-s` (packet size), `-I` (interface), `-p` (pattern), and `-R` (record route) are blocked; count/wait/interval are clamped to safe ranges with a warning; multicast, unspecified (`0.0.0.0`/`::`), and broadcast addresses (IPv4 last-octet `.255`) are rejected — note: directed broadcasts on non-standard subnets (e.g. `.127` on a `/25`) are not blocked without subnet-mask knowledge
- ✅ `printf FORMAT [ARGUMENT]...` — format and print data to stdout; supports `%s`, `%b`, `%c`, `%d`, `%i`, `%o`, `%u`, `%x`, `%X`, `%e`, `%E`, `%f`, `%F`, `%g`, `%G`, `%%`; format reuse for excess arguments; `%n` rejected (security risk); `-v` rejected
- ✅ `sed [-n] [-e SCRIPT] [-E|-r] [SCRIPT] [FILE]...` — stream editor for filtering and transforming text; uses RE2 regex engine; `-i`/`-f` rejected; `e`/`w`/`W`/`r`/`R` commands blocked
- ✅ `strings [-a] [-n MIN] [-t o|d|x] [-o] [-f] [-s SEP] [FILE]...` — print printable character sequences in files (default min length 4); offsets via `-t`/`-o`; filename prefix via `-f`; custom separator via `-s`
Expand Down
272 changes: 159 additions & 113 deletions allowedsymbols/symbols_builtins.go

Large diffs are not rendered by default.

170 changes: 170 additions & 0 deletions builtins/ping/builtin_ping_pentest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2026-present Datadog, Inc.

// Security-focused (pentest) tests for the ping builtin.
// Each test exercises a class of attack and verifies the implementation
// behaves safely (no crash, no hang, exit 0 or 1 only).

package ping_test

import (
"context"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

// ============================================================================
// GTFOBins / dangerous flag vectors
// ============================================================================

func TestPingPentestFloodRejected(t *testing.T) {
// -f flood is a DoS vector against the target network.
_, stderr, code := cmdRun(t, "ping -f 127.0.0.1")
assert.Equal(t, 1, code, "-f must be rejected as unknown flag")
assert.Contains(t, stderr, "ping:")
}

func TestPingPentestBroadcastRejected(t *testing.T) {
// -b allows pinging broadcast addresses, which can amplify traffic.
_, stderr, code := cmdRun(t, "ping -b 255.255.255.255")
assert.Equal(t, 1, code, "-b must be rejected as unknown flag")
assert.Contains(t, stderr, "ping:")
}

func TestPingPentestSizeRejected(t *testing.T) {
// -s would let the caller control payload size; not implemented.
_, stderr, code := cmdRun(t, "ping -s 65507 localhost")
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "ping:")
}

func TestPingPentestInterfaceRejected(t *testing.T) {
// -I interface binding not implemented.
_, stderr, code := cmdRun(t, "ping -I eth0 localhost")
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "ping:")
}

func TestPingPentestPatternRejected(t *testing.T) {
// -p pattern filling not implemented.
_, stderr, code := cmdRun(t, "ping -p ff localhost")
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "ping:")
}

func TestPingPentestRecordRouteRejected(t *testing.T) {
// -R record-route not implemented.
_, stderr, code := cmdRun(t, "ping -R localhost")
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "ping:")
}

// ============================================================================
// Integer overflow / boundary inputs
// ============================================================================

func TestPingPentestCountNegative(t *testing.T) {
// Negative count clamped to 1; should not hang.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// DNS resolution of "no-such-host-xyzzy.invalid" should fail quickly.
_, _, code := runScriptCtx(ctx, t, "ping -c -99 no-such-host-xyzzy.invalid")
assert.Equal(t, 1, code)
assert.NoError(t, ctx.Err(), "should not exceed 5-second deadline")
}

func TestPingPentestCountZero(t *testing.T) {
// -c 0 is clamped to 1; should not hang.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, _, _ = runScriptCtx(ctx, t, "ping -c 0 no-such-host-xyzzy.invalid")
assert.NoError(t, ctx.Err(), "clamped count=0 should not hang")
}

func TestPingPentestCountOverflow(t *testing.T) {
// Very large count clamped to 20.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, _, _ = runScriptCtx(ctx, t, "ping -c 2147483647 no-such-host-xyzzy.invalid")
assert.NoError(t, ctx.Err(), "large count should not hang (clamped + DNS fails fast)")
}

func TestPingPentestIntervalTooShort(t *testing.T) {
// Interval below 200ms floor; should not hang.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, _, _ = runScriptCtx(ctx, t, "ping -c 1 -i 1ns no-such-host-xyzzy.invalid")
assert.NoError(t, ctx.Err())
}

// ============================================================================
// Path / host injection
// ============================================================================

func TestPingPentestShellInjectionInHost(t *testing.T) {
// Verify that the literal string "$(id)" is treated as a hostname (DNS
// lookup fails) rather than triggering command substitution inside the
// builtin. The argument is single-quoted so the shell passes it verbatim
// to ping; only the resolver sees it.
_, _, code := cmdRun(t, `ping -c 1 '$(id)'`)
assert.Equal(t, 1, code)
}

func TestPingPentestLongHostname(t *testing.T) {
// A very long hostname should result in a DNS error, not a crash.
host := strings.Repeat("a", 10000) + ".invalid"
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, _, code := runScriptCtx(ctx, t, "ping -c 1 -- "+host)
assert.Equal(t, 1, code, "long hostname should fail with exit 1")
assert.NoError(t, ctx.Err(), "should not hang on long hostname")
}

func TestPingPentestEmptyHostname(t *testing.T) {
// Empty hostname (after shell expansion) should give a clear error.
_, stderr, code := cmdRun(t, `ping -c 1 ""`)
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "ping:")
}

// ============================================================================
// Context cancellation (timeout safety)
// ============================================================================

func TestPingPentestHeaderPrintedBeforeICMP(t *testing.T) {
// The PING header is emitted after DNS resolution but before opening the
// ICMP socket. This test verifies the invariant: stdout contains the
// header even when the ICMP step fails (e.g. EPERM or timeout).
// 192.0.2.1 is an RFC 5737 documentation address that never responds.
stdout, _, code := cmdRun(t, "ping -c 1 -W 100ms 192.0.2.1")
assert.Equal(t, 1, code)
assert.Contains(t, stdout, "PING 192.0.2.1 (192.0.2.1):", "PING header must appear in stdout even when ICMP fails")
}

func TestPingPentestContextTimeout(t *testing.T) {
// With a very short outer deadline, RunWithContext must exit promptly.
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
start := time.Now()
_, _, _ = runScriptCtx(ctx, t, "ping -c 10 -W 10s -i 5s 192.0.2.1")
elapsed := time.Since(start)
assert.Less(t, elapsed, 2*time.Second, "ping must stop when context is cancelled")
}

func TestPingPentestHardTotalTimeout(t *testing.T) {
// The builtin computes a hard total timeout. With maxed-out count and
// wait, the deadline must still be within the 120s cap.
// We use an unresolvable host so execution terminates on DNS failure.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// -c 20 -W 30s -i 60s = 20*(60+30)+5 = 1805s → capped at 120s. But DNS
// failure returns immediately.
_, _, code := runScriptCtx(ctx, t, "ping -c 20 -W 30s -i 60s no-such-host-xyzzy.invalid")
assert.Equal(t, 1, code)
assert.NoError(t, ctx.Err(), "should not exceed 5-second outer deadline")
}
Loading
Loading