Public research note
Baseten Truss
external_data Dockerfile
Baseten publishes Truss, an open-source packager
for serving machine-learning models. One field in
config.yaml, called external_data,
is supposed to be a URL that the image builder downloads with
curl during docker build. A double
quote in that URL left the quoted argument and became extra
shell. The fix is on main. A Windows dest-path
bug found while testing the same line is on main
too.
0
Critical
1
High
1
Medium
0
Low
1
Info
Hackerbane · Assessment Report
HB-AR-2026.2
Contents
- 1Executive summary
- 2Scope and method
- 3Findings summary
- 4HIGH-01: quote in external_data URL becomes extra shell
- 5MED-01: Windows dest path in a Linux image
- 6INFO-01: runtime path traversal is a different PR
- 7Remediation record
- 8Disclaimer
1 Executive summary
If you have not used Truss: it is an open-source tool from
Baseten. You describe a model in a folder. The important file is
config.yaml. truss build turns that
folder into a Docker image. Docker is the usual way to pack a
program and its files into one runnable image. A
Dockerfile is the recipe. Each
RUN line is a shell command that runs while the
image is being built.
This note is about one optional list in that yaml:
external_data. Each item has a url and
a local_data_path. The builder is supposed to
download the file at build time and put it under
/app/data/ in the image, so the model has its
weights without baking them into git.
The serving template did that with a shell line. On
main at commit 40ba448 the URL sat
inside double quotes and nothing else:
RUN mkdir -p {{ dst.parent }}; curl -L "{{ url }}" -o {{ dst }}
A " in the URL ends that quoted argument. What
follows is a new command. It runs during
truss build / docker build, inside the
build container, with the builder's permissions. The field still
looks like a URL. The person running the build may not have
written every line of the yaml. A fork, a shared template, or a
compromised config is enough.
I opened a patch for that. It is
PR 2616.
The first version reused an existing filter written for
Dockerfile ENV lines. cretz, who has
merge rights on the Baseten repo, pointed out that
RUN still goes through /bin/sh, so
backticks still run. The merged line uses
shlex.quote instead. cretz approved
on 25 August 2026 and merged on 26 August 2026 as
5ff9717. Baseten did not hire this review. The
mark on the cover names the project I read.
While writing tests for that PR, Windows CI turned
/app/data/... into C:\app\data\...
and put that string into the Linux image dest. That is
PR 2627,
merged 28 August 2026. It is MED-01 here.
1.1 The thing to fix first
| # |
ID |
Action |
Status on 7 Sep 2026 |
| 1 |
HIGH-01 |
Keep the dockerfile_shell_value filter on the external_data RUN line. Do not put that URL through the ENV filter. |
Merged. PR 2616. 5ff9717. |
| 2 |
MED-01 |
Build the dest with PurePosixPath. Do not resolve() a container path on the host. |
Merged. PR 2627. d075d47. |
| 3 |
INFO-01 |
Runtime download path traversal is a different file. Tracked as PR 2486. Not this review. |
Still open. |
2 Scope and method
This is not a full audit of Truss. I picked the
external_data build path because that is where a
yaml field that looks like a URL becomes a shell command. I did
not review the runtime downloader in download.py.
I did not review Baseten's hosted build. I did not treat other
Dockerfile interpolations as a second audit.
I use Truss locally to package models. I have not run this
change against Baseten SaaS. The bug is in Dockerfile generation
on truss build, so that is where I checked it.
| In scope |
truss/templates/server.Dockerfile.jinja:
the external_data_files RUN line.
dockerfile_env_value and
dockerfile_shell_value in
truss/util/jinja.py.
Dest construction in
truss/contexts/image_builder/serving_image_builder.py.
Tests in truss/tests/util/test_jinja.py and
truss/tests/contexts/image_builder/test_serving_image_builder.py.
|
| Out of scope |
Runtime download_external_data (PR 2486).
Baseten SaaS builds. Other RUN interpolations
in the same template, including user
build_commands. Live model serving.
|
| Method |
I read the template on 40ba448 and on
5ff9717. I read the first ENV-filter patch and
the review that rejected it for RUN. I read
the dest construction before and after PR 2627. I read the
unit tests, including the /bin/sh cases that
show the ENV filter still runs backticks.
|
| Threat |
Anyone who builds a Truss whose config.yaml
they did not fully write. The extra command runs at image
build, in the build container. That is the builder's
machine and the builder's credentials, not a later serving
request.
|
2.1 Checks executed
| Check |
Result |
Template on 40ba448 |
curl -L "{{ url }}". A quote in
url closes the argument.
|
| First patch (ENV filter) |
Stops ", \, and $.
Leaves backticks live in /bin/sh.
cretz wrote that down on the PR.
|
Merged patch 5ff9717 |
dockerfile_shell_value is
shlex.quote. The URL, dest, and
mkdir target are each one shell word.
|
| Unit tests on the patch |
test_shell_injection_metacharacters_in_url,
test_shell_value_quotes_backticks,
test_shell_value_is_one_posix_sh_word,
test_env_filter_still_executes_backticks_in_posix_sh,
plus the serving-image Dockerfile cases. POSIX shell
cases skip on Windows CI.
|
Dest path on 5ff9717 |
(Path("/app/data/") / local_data_path).resolve()
on the host. On Windows that becomes
C:\app\data\....
|
| Dest path after PR 2627 |
PurePosixPath("/app/data") / local_data_path.
No host resolve().
|
2.2 Limitations
I did not run a Baseten hosted build. I did not stand up a
malicious file server. The miss is in Dockerfile generation.
You can see it by reading the template and by running the unit
tests. The second hop (someone actually ships a yaml with a
quote in the URL) is why the field is a problem. I did not
demonstrate that hop against a third-party project.
PR 2486 is a different function. I did not re-test it. I name
it so nobody thinks the build-time quote fix closed the
runtime path-traversal report.
3 Findings summary
| ID |
Title |
Sev |
Kind |
Status |
| HIGH-01 |
Quote in external_data URL becomes extra shell |
High |
Security |
Remediations applied (PR 2616) |
| MED-01 |
Windows dest path written into a Linux image |
Medium |
Correctness |
Remediations applied (PR 2627) |
| INFO-01 |
Runtime path traversal is PR 2486, still open |
Info |
Note |
Out of scope; still open |
| Severity |
Meaning in this note |
Count |
| Critical |
Someone could steal funds or hurt people with what I looked at. Truss itself does not move money. None here. |
0 |
| High |
A field you treat as a URL can run extra commands at build. The quoting you think you have does not hold. |
1 |
| Medium |
A real miss on a common host OS. The image still builds. The file is not where the model looks. |
1 |
| Low |
Real, but limited, or it needs an unlikely setup. None here. |
0 |
| Informational |
On the record so the next person is not surprised. Not scored as a bug in this review. |
1 |
I called HIGH-01 High, not Critical, because nothing in Truss
spends funds, and because the command runs at image build, not
on a later serving request. Docker build already runs the
recipe you gave it. The damage is that a yaml field that looks
like a URL can add commands the builder did not mean to run.
If you wrote every character of that yaml yourself, you already
had that power. The case that matters is the yaml you did not
fully write.
Hackerbane · Assessment Report
HIGH-01
4 HIGH-01
HIGH-01 A quote in the external_data URL becomes extra shell during docker build
High
Security
Remediations applied
truss/templates/server.Dockerfile.jinja, external_data RUN
· truss/util/jinja.py ·
serving_image_builder.py dest list ·
tests in test_jinja.py
This is the finding that matters. The builder walks
external_data and emits one
RUN per file. On 40ba448 the URL
was interpolated into a double-quoted shell argument. That
is not quoting. It is wrapping. The first
" inside the value ends the wrap.
The code on main before the patch
RUN mkdir -p {{ dst.parent }}; curl -L "{{ url }}" -o {{ dst }}
The public example from the pull request is enough to see
the break. A yaml value of the form
http://example.com/x" ; command ; echo "
produces a RUN where command is
no longer an argument to curl. It is a
sibling command in the same shell line. It runs when the
image is built.
The first fix was the wrong grammar
Truss already had dockerfile_env_value for
ENV SERVER_START_CMD. That filter follows
BuildKit's double-quoted ENV rules. It escapes
", \, and $.
RUN is not ENV. Docker still hands the line
to /bin/sh. In that shell, backticks and
$() still run. cretz wrote the
review in those words. A URL like
http://example.com/`cmd` still executed
under the ENV filter. The test
test_env_filter_still_executes_backticks_in_posix_sh
keeps that fact on the record so nobody puts the ENV
filter back on this line.
The merged line
RUN mkdir -p {{ dst.parent | string | dockerfile_shell_value }}; curl -L {{ url | dockerfile_shell_value }} -o {{ dst | string | dockerfile_shell_value }}
dockerfile_shell_value is
shlex.quote. It uses single quotes. That
stops the substitutions /bin/sh would still
make after ENV quoting. Line breaks are rejected without
echoing the value, because a URL can embed a secret.
Why this is High
The check you think you have is "this field is a URL, so
curl fetches it." That check does not hold when the value
can close the quote. A pass on a config review that only
looks at the host name is a miss. I did not call it
Critical. No funds move. The process that runs the extra
command is the build you already started.
Hackerbane · Assessment Report
MED-01 · INFO-01
5 MED-01
MED-01 Windows resolve() writes a drive path into a Linux image dest
Medium
Correctness
Remediations applied
serving_image_builder.py dest construction on
5ff9717. Fixed in
PR 2627.
The image is Linux. The dest is supposed to be
/app/data/<local_data_path>. The builder
used the host's pathlib.Path and called
resolve() on it:
data_dir = Path("/app/data/")
(data_dir / ext_file.local_data_path).resolve()
On Windows that becomes
C:\app\data\weights\model.bin. That string
went into the Dockerfile curl -o line. The
file is then not where the model looks. I hit this while
adding tests for PR 2616. Windows CI produced the drive
path, so those tests dropped the
/app/data assertion just to stay green. The
generated Dockerfile was still wrong for anyone running
truss build on Windows with
external_data.
What the follow-up did
truss migrate already built the same path
with PurePosixPath. PR 2627 matches that.
After the merge the dest is a POSIX container path on
every host.
container_data_dir = PurePosixPath("/app/data")
container_data_dir / ext_file.local_data_path
Why this is Medium
The image still builds. The weights land in the wrong
place, so the model misses them. That is a quiet failure
on a host OS a lot of contributors use. It is not extra
shell. It is not a quote breakout.
6 INFO-01
INFO-01 Runtime path traversal is a different PR, still open
Informational
Out of scope
PR 2486
by lollinng. download_external_data. Open on
7 September 2026.
The runtime downloader checks that
local_data_path stays inside the data
directory before it writes. The check uses
path.parents without resolving
.. first, so a value like
../evil.bin can pass the guard and still
write outside the directory after
resolve(). That is a different function
from the Dockerfile curl line. PR 2616
does not close it. I am not scoring it here. I am
saying it is still open, so nobody should treat the
build-time quote fix as the whole
external_data story.
7 Remediation record
| Date |
Event |
| 22 Aug 2026 |
PR 2616 opened. First patch used
dockerfile_env_value on the
curl line.
|
| 24 Aug 2026 |
cretz reviewed. ENV grammar leaves
backticks live in /bin/sh. Asked
whether the change had been run against Baseten
SaaS.
|
| 25 Aug 2026 |
Switched to dockerfile_shell_value
(shlex.quote). Added backtick,
$(), and /bin/sh
execution tests. Said I use Truss locally and have
not run the change on Baseten SaaS.
cretz approved.
|
| 26 Aug 2026 |
PR 2616 merged by cretz as
5ff9717. HIGH-01 remediations
applied on main.
|
| 28 Aug 2026 |
PR 2627 merged by cretz as
d075d47. MED-01 remediations
applied on main.
|
| 7 Sep 2026 |
This note. HIGH-01 and MED-01 are on main.
PR 2486 still open.
|
To retest HIGH-01, run the unit tests in section 2.1,
including
test_env_filter_still_executes_backticks_in_posix_sh
so the ENV filter is not put back on a
RUN line. To retest MED-01, generate a
Dockerfile with external_data on Windows
and confirm the dest is /app/data/....
Hackerbane · Assessment Report
8 · Disclaimer
8 Disclaimer
Read this before you treat anything above as advice.
This file is a public research note. Baseten did not hire
Hackerbane. There is no services agreement, statement of work,
or client relationship behind it. The Baseten mark on the cover
names the project I read. It is not a partnership badge.
What you have is an opinion about the files and commits named
in section 2. It is not a certificate. It is not an endorsement
of Baseten, of Truss, or of the patches. It is not a claim that
the rest of Truss is safe, or that it is broken.
I am not telling you to merge, deploy, buy, or sell anything.
This is not investment advice, legal advice, or a security
guarantee. If you ship software, you own that risk. If you run
Truss, you own that risk. If you build a yaml you did not
write, you own that risk.
Hackerbane and I accept no liability for how anyone uses this
note, for bugs it does not name, or for loss that follows from
relying on it. Findings can be wrong, incomplete, or already
stale the day after the review commit. Code added after that
commit was not reviewed. A retest covers only the checks it
names.
You may share this file. Sharing it does not create a contract
and does not make the reader a client.
This note is provided as-is, as available, with all faults. To the maximum extent the law allows, Hackerbane disclaims every warranty, express or implied, including merchantability, fitness for a particular purpose, title, and non-infringement. No promise that this text is accurate, complete, or free of error, or that any error will be fixed.
Nobody may rely on this note as a professional engagement. It is not financial, tax, legal, or regulatory advice. No third party is a beneficiary of it.
Severity words in this note
| Critical |
Someone could steal funds or hurt people with what I looked at. |
| High |
The check you think you have does not work in the common case. |
| Medium |
A real miss, less common in the output I care about here. |
| Low |
Real, but limited, or it needs an unlikely setup. |
| Informational |
On the record. Not scored as a bug. |
The coloured bars are labels. The word High or Medium is what
counts.
hello@hackerbane.com · security@hackerbane.com · hackerbane.com