Hackerbane
Baseten
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
Document HB-AR-2026.2  /  public source review, Dockerfile generation
What this is A research note and an opinion. Baseten did not hire Hackerbane for this.
System Baseten Truss, an open-source packager for serving models
Scope external_data interpolated into a Dockerfile RUN that shells curl
Review commit main @ 40ba448 (confirmed) · patch 5ff9717 · follow-up d075d47
Upstream PR 2616 (merged)  /  PR 2627 (merged)
Date 7 September 2026
Author Barney Chambers, Hackerbane
hackerbane.com hello@hackerbane.com HB-AR-2026.2
Hackerbane · Assessment Report HB-AR-2026.2

Contents

  1. 1Executive summary
  2. 2Scope and method
  3. 3Findings summary
  4. 4HIGH-01: quote in external_data URL becomes extra shell
  5. 5MED-01: Windows dest path in a Linux image
  6. 6INFO-01: runtime path traversal is a different PR
  7. 7Remediation record
  8. 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