Skip to content

Fix shell injection in external_data Dockerfile curl RUN lines - #2616

Merged
cretz merged 5 commits into
basetenlabs:mainfrom
BarneyChambers:fix/external-data-dockerfile-url-escaping
Aug 26, 2026
Merged

Fix shell injection in external_data Dockerfile curl RUN lines#2616
cretz merged 5 commits into
basetenlabs:mainfrom
BarneyChambers:fix/external-data-dockerfile-url-escaping

Conversation

@BarneyChambers

@BarneyChambers BarneyChambers commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

What

Truss can download remote files at image build time via external_data in config.yaml. The serving image builder turns each entry into a Dockerfile RUN line that shells out to curl:

RUN mkdir -p /app/data/weights; curl -L "http://example.com/model.bin" -o /app/data/weights/model.bin

The url field is interpolated into that shell command with no escaping beyond wrapping it in double quotes. If the URL contains ", the shell treats everything after the quote as a new command. A config like this:

external_data:
  - url: 'http://example.com/x" ; malicious_command ; echo "'
    local_data_path: weights/model.bin

generates:

RUN mkdir -p /app/data/weights; curl -L "http://example.com/x" ; malicious_command ; echo "" -o /app/data/weights/model.bin

malicious_command runs during truss build / docker build, inside the build container, with the builder's permissions.

This is a build-time injection in user-supplied config. It matters when the person running the build did not write every field in config.yaml themselves: a forked truss repo, a shared template, a third-party example, or a compromised config checked into source control.

After the fix, the same URL stays inside one quoted argument:

RUN mkdir -p "/app/data/weights"; curl -L "http://example.com/x\" ; malicious_command ; echo \"" -o "/app/data/weights/model.bin"

Related to #2486, which fixes path traversal in the runtime download path (download.py). This PR covers the separate Dockerfile build path.

Why

I was trying Truss with external_data to pull model weights from a URL, same pattern I use in other serving setups. Before pushing, I looked at the generated Dockerfile to see what actually runs during build. The curl line embeds the URL directly in a shell RUN with no escaping, so I tried a URL with a " in it and confirmed it breaks out of the quoted argument.

How

Apply the existing dockerfile_env_value Jinja filter (already used for ENV SERVER_START_CMD) to the URL, destination path, and mkdir target in server.Dockerfile.jinja. That filter escapes ", \, and $ for double-quoted shell/ENV contexts. No new escaping helper.

Testing

Added:

  • test_shell_injection_metacharacters_in_url in truss/tests/util/test_jinja.py
  • test_external_data_url_shell_metacharacters_escaped_in_dockerfile in truss/tests/contexts/image_builder/test_serving_image_builder.py
uv run pytest truss/tests/util/test_jinja.py -v
uv run pytest truss/tests/contexts/image_builder/test_serving_image_builder.py::test_external_data_url_shell_metacharacters_escaped_in_dockerfile -v
uv run pytest truss/tests/contexts/image_builder/test_serving_image_builder.py::test_serving_image_dockerfile_from_user_base_image -v

external_data URLs were embedded unescaped in shell RUN curl commands.
Use the existing dockerfile_env_value filter for URL and destination paths.
The serving Dockerfile also contains a curl for uv bootstrap; scope the
assertion to the external_data RUN line only.

@cretz cretz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution! Left a couple of comments

Comment thread truss/templates/server.Dockerfile.jinja Outdated
{%- if external_data_files %}
{% for url, dst in external_data_files %}
RUN mkdir -p {{ dst.parent }}; curl -L "{{ url }}" -o {{ dst }}
RUN mkdir -p {{ dst.parent | string | dockerfile_env_value }}; curl -L {{ url | dockerfile_env_value }} -o {{ dst | string | dockerfile_env_value }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dockerfile_env_value implements buildkit's ENV grammar, but this is a shell RUN which backticks still substitute, so http://example.com/`cmd` gets through. shlex.quote() is probably a better approach

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. Switched that line to a dockerfile_shell_value filter that is shlex.quote, and added a backtick case plus /bin/sh execution tests.

@cretz cretz Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, are a Baseten user? Have you been able to test this against Baseten SaaS platform (why or why not)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use Truss locally to package models for serving. I have not run this change against Baseten SaaS. The bug is in Dockerfile generation on truss build, so I checked it there

I have signed up for Baseten previously but I am still in the waiting room. Thank you for reviewing my PR!

dockerfile_env_value leaves backticks live in /bin/sh, so a URL like
http://example.com/`cmd` still executes during image build.

@cretz cretz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like CI is failing lint on this file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. ruff format on that file, and skipped the /bin/sh tests on Windows CI.
Should be good to go now!

The /bin/sh execution tests need dash, which Windows CI does not have.
Also stop requiring /app/data in the Dockerfile line; Path.resolve()
turns that into a drive path on Windows.
@cretz
cretz merged commit 5ff9717 into basetenlabs:main Aug 26, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants