
“that’s not jam, it’s strawberries and goo” - David Mitchell
I was upping my test coverage for a Go project when something scrolled past that caught my eye.
$ go test --coverprofile=coverage.out ./...
[... etc ...]
ok mypacakge/cmd/foo 0.0.33s coverage: 83.3%
ok mypackage/service/foo 0.053s coverage: 89.7%
? mypackage/frontend/node_modules/flatted/golang/pkg/flatted [no test files]
ok mypackage/utils/ 0.911s coverage: 92.4%
[... etc ...]
Did go test just traverse my node_modules?
The Full-Stack Dev
More developers are pushed today to be a jack-of-all trades. It’s become common to expect some positions to cover frontend and backend development, especially considering the hyped agility that well integrated coding agents can provide. I’d also wager coding agents can lure developers far too deep outside their expertise, confidently encouraging them, “Jump!”.
While the JS ecosystem has been maturing on the backend side of development, more experienced full-stack developers become familiar with a few backend languages and frontend frameworks; choosing the best fit for the given project.
A popular pairing has been Go with modern JS frameworks like Angular, React, Vue, etc. Go even has an amazing feature go embed that allows you to embed files into your binary, accessible by a small file-system like interface. You can deliver your entire application, backend and frontend, in a single binary.
ACE in the Node

Regardless of whether you embed your frontend or serve it from the file-system, how you actually structure your project, the directory hierarchy, could lead to arbitrary code execution in your most critical workflows, building and testing. You will see this critical flaw in many tutorials, some quietly side-step the issue without a word, most recommend the footgun without realizing it’s loaded.
PoC
You can try this PoC directly on your machine, but I prefer to build an image to use as a temporary test environment. So understand the container runtime environment is not required to trigger this ACE.
First, build an image. You can copy/paste this as one big command
(ignoring $), or create a Containerfile or Dockerfile with the
contents of the heredoc.
$ podman build -t pwnme - <<'EOF'
FROM docker.io/node:24-alpine
WORKDIR /app
RUN mkdir frontend && cd frontend \
&& npm init --yes && npm install --save-dev eslint
RUN apk add -q --no-cache go && go mod init mypackage
ENTRYPOINT ["/usr/bin/go"]
EOF
Now we can run our tests. Does go test traverse node_modules?
$ podman run --rm -it localhost/pwnme test ./...
? mypackage/frontend/node_modules/flatted/golang/pkg/flatted [no test files]
Can we touch grass with go test?
$ podman run --rm --entrypoint /bin/sh -it localhost/pwnme
/app # cat <<'EOF' > /app/frontend/node_modules/flatted/golang/pkg/flatted/flatted_test.go
> package flatted
> import "os"
> func init() { _ = os.WriteFile("/pwned", []byte("booo!\n"), 0666) }
> EOF
/app # go clean -testcache && go test ./...
ok mypackage/frontend/node_modules/flatted/golang/pkg/flatted 0.002s [no tests to run]
/app # cat /pwned
booo!
What about go generate?
$ podman run --rm --entrypoint /bin/sh -it localhost/pwnme
/app # cat <<'EOF' > /app/frontend/node_modules/flatted/golang/pkg/flatted/gen.go
> package flatted
> //go:generate touch /go-go-generate
> EOF
/app # go generate ./...
/app # ls -l /go-go-generate
-rw-r--r-- 1 root root 0 Apr 9 00:12 /go-go-generate
OK, you get the point, there are probably quite a few ways to get an ACE with this pattern; how do we avoid it?
Directory and file names that begin with "." or "_" are ignored by the go tool, as are directories named "testdata".
go doc cmd/go
Probably the easiest to overlook and easiest to implement, if you don’t want Go crawling other directories, you should prefix the the directory with an underscore. It’s simple, yet annoyingly easy to trip over.
Hierarchy

Mon Mothma - Andor
Ultimately, I believe the best practice for maintaining a frontend and backend is segmenting them into their own repositories. You won’t need to fuss with naming conventions, but many developers likely view this as a hurdle in their workflow. We need two merges to two repositories that must stay in sync. We need two CI/CD pipelines, one of which that can securely consume build artifacts of another.
Keeping the repositories and build pipelines separate will grant you the greatest isolation and limit the ability for one project to compromise another. It really shouldn't be much of a bother to setup safe consuming of upstream packages, but I am quite sure that is not the norm for most developers; especially those who might be stretched-thin in a full-stack role.
Embedding
The go embed is such a nice feature, but consider the security
requirement that it may not traverse up the directory tree, only down.
This is to prevent accidental leaks of say, /etc/shadow.
So even though you can trigger this ACE simply due to a poor
directory structure, backends that seek to embed their frontend
require the target to exist in the Go project directory.
While your repository root could be two directories, a README.md, and
a Makefile, most developers aren’t carefully making that choice.
Rather, the common boilerplate in tutorials is letting Go own the
project root and keeping a frontend/ directory alongside.
npm
My intention of this post is to highlight how any number of languages, packagers, or tools paired with poor project structure could result in surprising attacks. I am not here to point the finger at any developer, package, etc. I was just rather shocked to see it myself.