Files
kage/browser/pool_test.go
T
Duc-Tam Nguyen dab6c11ea8 Keep the Chrome sandbox on by default
kage launched Chrome with --no-sandbox unconditionally, which turns off the
browser's main security boundary for every run, including ordinary desktop
use where the sandbox works fine. Since kage renders pages from the open web,
a renderer exploit could then reach the host. Reported in #10.

Keep the sandbox on by default and drop it only where it genuinely cannot
initialize: inside a container, or when running as root (Chrome refuses to
start a sandbox as root). Containers are detected from IN_DOCKER or the
/.dockerenv marker, and there kage also sets --disable-dev-shm-usage because
the default 64 MB /dev/shm is too small for the renderer on large pages.
Whenever the sandbox is dropped kage says so on stderr, so it is never silent.

Thanks to Dimitrios Prasakis for the report and to the commenter on Hacker
News who suggested the IN_DOCKER opt-in.
2026-06-15 12:24:55 +07:00

118 lines
3.2 KiB
Go

package browser
import (
"context"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
)
func TestLookChromeReadsEnv(t *testing.T) {
t.Setenv("KAGE_CHROME", "/custom/chrome")
bin, ok := LookChrome()
if !ok || bin != "/custom/chrome" {
t.Fatalf("LookChrome() = %q, %v; want /custom/chrome, true", bin, ok)
}
}
func TestEnvBool(t *testing.T) {
cases := []struct {
in string
set bool
wantVal bool
wantOk bool
}{
{"", false, false, false},
{"1", true, true, true},
{"true", true, true, true},
{"TRUE", true, true, true},
{"yes", true, true, true},
{"on", true, true, true},
{"0", true, false, true},
{"false", true, false, true},
{"off", true, false, true},
{"no", true, false, true},
{"docker", true, true, true}, // any other non-empty value is true
{" true ", true, true, true}, // trimmed
}
for _, c := range cases {
if c.set {
t.Setenv("KAGE_TEST_BOOL", c.in)
} else {
_ = os.Unsetenv("KAGE_TEST_BOOL")
}
val, ok := envBool("KAGE_TEST_BOOL")
if val != c.wantVal || ok != c.wantOk {
t.Errorf("envBool(%q) = (%v, %v); want (%v, %v)", c.in, val, ok, c.wantVal, c.wantOk)
}
}
}
func TestDisableSandboxDefaultKeepsItOn(t *testing.T) {
// Not in a container and not root, the sandbox stays on. (When the test
// itself runs as root, e.g. some CI containers, "root" is the honest
// reason; accept that rather than asserting a false negative.)
t.Setenv("IN_DOCKER", "")
off, reason := disableSandbox()
if isRoot() || inContainer() {
if !off {
t.Errorf("disableSandbox() = false as root/container; want true")
}
return
}
if off {
t.Errorf("disableSandbox() = true (%q) on a normal host; want sandbox kept on", reason)
}
}
func TestInContainerHonorsEnv(t *testing.T) {
t.Setenv("IN_DOCKER", "1")
if !inContainer() {
t.Errorf("inContainer() = false with IN_DOCKER=1; want true")
}
}
func TestDisableSandboxContainer(t *testing.T) {
t.Setenv("IN_DOCKER", "true")
if off, reason := disableSandbox(); !off || reason != "container" {
t.Errorf("in container: got (%v, %q); want (true, container)", off, reason)
}
}
func TestRenderCapturesFinalDOM(t *testing.T) {
if testing.Short() {
t.Skip("render test drives Chrome; skipped under -short")
}
if _, ok := LookChrome(); !ok {
t.Skip("no Chrome/Chromium found; skipping render test")
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
// A page whose visible content is built by JavaScript: only a real
// browser render captures the injected node.
_, _ = w.Write([]byte(`<!doctype html><html><body>
<div id="app"></div>
<script>document.getElementById("app").textContent = "rendered-by-js";</script>
</body></html>`))
}))
defer srv.Close()
p := New(Options{Headless: true, Workers: 1, Settle: 300 * time.Millisecond, RenderTimeout: 20 * time.Second})
defer func() { _ = p.Close() }()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
res, err := p.Render(ctx, srv.URL)
if err != nil {
t.Fatalf("render: %v", err)
}
if !strings.Contains(res.HTML, "rendered-by-js") {
t.Errorf("render did not capture the JS-built DOM:\n%s", res.HTML)
}
}