Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
299 changes: 299 additions & 0 deletions interp/builtins/cat/builtin_cat_pentest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
// 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.

package cat_test

import (
"context"
"math"
"os"
"path/filepath"
"strings"
"testing"
"time"

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

"github.com/DataDog/rshell/interp"
"github.com/DataDog/rshell/interp/builtins/cat"
"github.com/DataDog/rshell/interp/builtins/testutil"
)

const pentestTimeout = 10 * time.Second

func catRun(t *testing.T, script, dir string, extraPaths ...string) (string, string, int) {
t.Helper()
paths := append([]string{dir}, extraPaths...)
return testutil.RunScript(t, script, dir, interp.AllowedPaths(paths))
}

func catRunCtx(ctx context.Context, t *testing.T, script, dir string) (string, string, int) {
t.Helper()
return testutil.RunScriptCtx(ctx, t, script, dir, interp.AllowedPaths([]string{dir}))
}

func mustNotHang(t *testing.T, fn func()) {
t.Helper()
done := make(chan struct{})
go func() {
fn()
close(done)
}()
select {
case <-done:
case <-time.After(pentestTimeout):
t.Fatal("operation did not complete within timeout")
}
}

// --- Flag and argument injection ---

func TestCatPentestUnknownLongFlag(t *testing.T) {
dir := t.TempDir()
_, stderr, code := catRun(t, "cat --no-such-flag file.txt", dir)
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "cat:")
}

func TestCatPentestUnknownShortFlag(t *testing.T) {
dir := t.TempDir()
_, stderr, code := catRun(t, "cat -f file.txt", dir)
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "cat:")
}

func TestCatPentestFlagViaExpansion(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "file.txt", "ok\n")
_, stderr, code := catRun(t, `for flag in -f; do cat $flag file.txt; done`, dir)
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "cat:")
}

func TestCatPentestDoubleDashThenFlagLikeFile(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "-n", "content\n")
stdout, _, code := catRun(t, "cat -- -n", dir)
assert.Equal(t, 0, code)
assert.Equal(t, "content\n", stdout)
}

func TestCatPentestMultipleStdinDash(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "src.txt", "hello\n")
stdout, _, code := catRun(t, "cat - - < src.txt", dir)
assert.Equal(t, 0, code)
assert.Equal(t, "hello\n", stdout)
}

// --- Path and filename edge cases ---

func TestCatPentestNonexistentFile(t *testing.T) {
dir := t.TempDir()
_, stderr, code := catRun(t, "cat does_not_exist", dir)
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "cat:")
}

func TestCatPentestDirectoryAsFile(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.Mkdir(filepath.Join(dir, "subdir"), 0755))
_, stderr, code := catRun(t, "cat subdir", dir)
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "cat:")
}

func TestCatPentestEmptyStringFilename(t *testing.T) {
dir := t.TempDir()
_, stderr, code := catRun(t, `cat ""`, dir)
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "cat:")
}

func TestCatPentestPathTraversal(t *testing.T) {
dir := t.TempDir()
outer := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(outer, "secret.txt"), []byte("secret"), 0644))
outerPath := strings.ReplaceAll(filepath.Join(outer, "secret.txt"), `\`, `/`)
_, stderr, code := catRun(t, "cat "+outerPath, dir)
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "cat:")
}

// --- Long lines ---

func TestCatPentestLineAtCapMinus1(t *testing.T) {
dir := t.TempDir()
content := make([]byte, cat.MaxLineBytes-1)
for i := range content {
content[i] = 'a'
}
content = append(content, '\n')
require.NoError(t, os.WriteFile(filepath.Join(dir, "file.txt"), content, 0644))
stdout, _, code := catRun(t, "cat -n file.txt", dir)
assert.Equal(t, 0, code)
assert.Contains(t, stdout, " 1\t")
}

func TestCatPentestLineAtCapExact(t *testing.T) {
dir := t.TempDir()
content := make([]byte, cat.MaxLineBytes)
for i := range content {
content[i] = 'a'
}
require.NoError(t, os.WriteFile(filepath.Join(dir, "file.txt"), content, 0644))
_, _, code := catRun(t, "cat -n file.txt", dir)
assert.Equal(t, 1, code)
}

func TestCatPentestLineAtCapPlus1(t *testing.T) {
dir := t.TempDir()
content := make([]byte, cat.MaxLineBytes+1)
for i := range content {
content[i] = 'a'
}
require.NoError(t, os.WriteFile(filepath.Join(dir, "file.txt"), content, 0644))
_, stderr, code := catRun(t, "cat -n file.txt", dir)
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "cat:")
}

// --- Memory / resource exhaustion ---

func TestCatPentestSmallFileRawMode(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "tiny.txt", "small\n")
mustNotHang(t, func() {
stdout, _, code := catRun(t, "cat tiny.txt", dir)
assert.Equal(t, 0, code)
assert.Equal(t, "small\n", stdout)
})
}

func TestCatPentestManyFiles(t *testing.T) {
dir := t.TempDir()
var args []string
for i := 0; i < 200; i++ {
name := "f" + strings.Repeat("0", 3-len(string(rune('0'+i/100)))) + string(rune('0'+i/100)) + string(rune('0'+i%100/10)) + string(rune('0'+i%10)) + ".txt"
writeFile(t, dir, name, "line\n")
args = append(args, name)
}
mustNotHang(t, func() {
_, _, code := catRun(t, "cat "+strings.Join(args, " "), dir)
assert.Equal(t, 0, code)
})
}

func TestCatPentestLargeFileNumbered(t *testing.T) {
dir := t.TempDir()
var sb strings.Builder
for i := 0; i < 10000; i++ {
sb.WriteString("line\n")
}
writeFile(t, dir, "big.txt", sb.String())
mustNotHang(t, func() {
stdout, _, code := catRun(t, "cat -n big.txt", dir)
assert.Equal(t, 0, code)
assert.Contains(t, stdout, " 10000\t")
})
}

// --- Special files / infinite sources ---

func TestCatPentestDevNull(t *testing.T) {
if os.DevNull == "NUL" {
// Windows reserved device names (NUL, CON, etc.) are blocked by the
// sandbox to prevent hangs — same as head's TestHeadWindowsReservedName.
dir := t.TempDir()
mustNotHang(t, func() {
_, stderr, code := catRun(t, "cat NUL", dir)
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "cat:")
})
return
}
dir := t.TempDir()
mustNotHang(t, func() {
stdout, _, code := catRun(t, "cat "+os.DevNull, dir, filepath.Dir(os.DevNull))
assert.Equal(t, 0, code)
assert.Equal(t, "", stdout)
})
}

func TestCatPentestContextCancelledDuringRaw(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "file.txt", strings.Repeat("x\n", 10000))
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
mustNotHang(t, func() {
catRunCtx(ctx, t, "cat file.txt", dir)
})
}

func TestCatPentestContextCancelledDuringLines(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "file.txt", strings.Repeat("x\n", 10000))
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
mustNotHang(t, func() {
catRunCtx(ctx, t, "cat -n file.txt", dir)
})
}

// --- Integer edge cases in line numbering ---

func TestCatPentestLineNumberMaxInt(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "file.txt", "hello\n")
mustNotHang(t, func() {
stdout, _, code := catRun(t, "cat -n file.txt", dir)
assert.Equal(t, 0, code)
assert.Equal(t, " 1\thello\n", stdout)
})
}

func TestCatPentestLineNumberWidthOverflow(t *testing.T) {
_ = math.MaxInt64
dir := t.TempDir()
writeFile(t, dir, "file.txt", "hello\n")
stdout, _, code := catRun(t, "cat -n file.txt", dir)
assert.Equal(t, 0, code)
assert.Contains(t, stdout, " 1\t")
}

// --- Behavior matching ---

func TestCatPentestSqueezeOnlyAffectsBlankLines(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "file.txt", "a\n \n \nb\n")
stdout, _, code := catRun(t, "cat -s file.txt", dir)
assert.Equal(t, 0, code)
assert.Equal(t, "a\n \n \nb\n", stdout)
}

func TestCatPentestCRLFNotBlank(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "file.txt", "a\n\r\n\r\nb\n")
stdout, _, code := catRun(t, "cat -s file.txt", dir)
assert.Equal(t, 0, code)
assert.Equal(t, "a\n\r\n\r\nb\n", stdout)
}

func TestCatPentestShowEndsCRLFNoV(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "file.txt", "a\rb\r\nc\n\r\nd\r")
stdout, _, code := catRun(t, "cat -E file.txt", dir)
assert.Equal(t, 0, code)
assert.Equal(t, "a\rb^M$\nc$\n^M$\nd\r", stdout)
}

func TestCatPentestShowEndsCRLFWithV(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "file.txt", "a\r\nb\n")
stdout, _, code := catRun(t, "cat -vE file.txt", dir)
assert.Equal(t, 0, code)
assert.Equal(t, "a^M$\nb$\n", stdout)
}
Loading