Skip to content

fix: sanitize newlines in docker env dict values and v2 env rendering - #5692

Open
feiiiiii5 wants to merge 4 commits into
bentoml:mainfrom
feiiiiii5:fix/docker-env-newline-injection
Open

fix: sanitize newlines in docker env dict values and v2 env rendering#5692
feiiiiii5 wants to merge 4 commits into
bentoml:mainfrom
feiiiiii5:fix/docker-env-newline-injection

Conversation

@feiiiiii5

@feiiiiii5 feiiiiii5 commented Aug 15, 2026

Copy link
Copy Markdown

What does this PR address?

Values supplied through the legacy docker.env dict (bentofile.yaml -> docker.env: {KEY: VALUE}) and through the new envs path are interpolated into generated Dockerfile ARG lines. A value containing a newline breaks out of the line and injects an arbitrary Dockerfile instruction (e.g. RUN ...), which executes during docker build.

Fixes #5656.

Fix

Sanitize only at the Dockerfile render boundary, so the config model keeps user values byte-for-byte:

  • base.j2 (legacy dict path): ARG {{ key|normalize_line }}={{ value|normalize_line|bash_quote }} and the matching ENV reference.
  • base_v2.j2 (envs path): add normalize_line before bash_quote for the value.

Test

  • Newline injection regression renders both paths and asserts the value stays on the quoted ARG line.
  • New model-preservation regression asserts DockerOptions.env keeps tabs, repeated spaces, and surrounding whitespace unchanged.
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 PYTHONPATH=src python3 -m pytest tests/unit/_internal/container -q
24 passed

Before submitting:

  • Does the Pull Request follow Conventional Commits specification naming?
  • Does the code follow BentoML's code style, pre-commit run -a script has passed?
  • Did you read through contribution guidelines and follow development guidelines?
  • Did your changes require updates to the documentation? Have you updated those accordingly?
  • Did you write tests to cover your changes?

Remote clients serialising a list[Path] field with URL-backed values wrote
each URL into the ``data`` dict under the same field name, so only the last
URL survived and earlier values were silently dropped before reaching the
service.

URL-backed values are now emitted as repeated text parts (httpx files with
filename=None; aiohttp add_field calls), so every value in a list field
produces its own same-name part. The server already aggregates repeated
field names via form.getlist.
Values from the legacy docker.env dict were interpolated into ARG/ENV
Dockerfile lines without sanitization; a value containing a newline broke
out of the line and injected an arbitrary Dockerfile instruction executed
during docker build. base_v2.j2 had the same flaw for the new envs path:
bash_quote does not remove newlines.

Collapse whitespace (including newlines) on dict env keys and values in
_convert_env, and normalize v2 env values before bash_quote, matching the
hardened sibling paths (system_packages, base_image, envs names).

Addresses bentoml#5656.
@feiiiiii5
feiiiiii5 requested a review from a team as a code owner August 15, 2026 09:54
@feiiiiii5
feiiiiii5 requested review from ssheng and removed request for a team August 15, 2026 09:54

@shashvat-singham shashvat-singham left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ran the branch — tests/unit/_internal/client/test_multipart.py is 3 passed.

The template fix is the right one, and worth keeping. Adding normalize_line to the ARG value is what actually closes #5656: bash_quote alone could never have, because Docker parses the Dockerfile line by line, so a raw newline in the value ends the ARG instruction no matter how the shell would later quote it. And it's sufficient on its own — the value appears only on the ARG line; the ENV line just references ${NAME}, so there's no second path a value reaches.

Given that, the _convert_env change looks redundant for security but lossy for data. It collapses all whitespace in the config model, not at the render boundary, so it silently rewrites legitimate values that were never a problem:

'a\nRUN echo pwned'    -> 'a RUN echo pwned'    # the case you're targeting
'a\tb'                 -> 'a b'                 # tab destroyed
'-Xmx1g  -Xms512m'     -> '-Xmx1g -Xms512m'     # double space collapsed
' padded '             -> 'padded'              # surrounding whitespace stripped

Because it mutates BentoEnvSchema rather than the generated Dockerfile, the altered value is what the service sees at runtime too — so an env var whose value legitimately contains a tab or repeated spaces (JAVA_OPTS, a format string, an indented cert/PEM-ish blob) is quietly changed in a context where nothing unsafe was happening. That's a behaviour change for correct inputs in exchange for defence the template already provides.

My suggestion would be to drop the _convert_env hunk and keep the escaping at the point of rendering. If you'd rather have belt-and-braces at the model layer, rejecting a value containing a newline would be better than silently normalising it — a user who put a newline in an env value has made a mistake and would want to hear about it, and it leaves tabs and spacing alone.

Separately: this PR appears to contain #5691. The http.py / proxy2.py multipart changes here are the same fix as "send one multipart part per URL in list file fields", and the new test file covers that rather than the env sanitisation. If they're stacked deliberately that's fine, but as it stands the title and the linked issue (#5656) only describe the env half, so a reviewer landing here won't expect the client changes — and if #5691 merges first this will conflict. Worth either splitting them or retitling to say it carries both.

@feiiiiii5

Copy link
Copy Markdown
Author

Addressed in commit a274616:

  • Dropped the _convert_env whitespace collapse. The config model now preserves tabs, repeated spaces, and surrounding whitespace; test_docker_options_env_preserves_value_whitespace pins that.
  • Escaping moved entirely to the Dockerfile render boundary. base.j2 (legacy dict path) now applies normalize_line to the name and normalize_line | bash_quote to the value; base_v2.j2 keeps the same value treatment. A newline-bearing value stays on the quoted ARG line in both paths.
  • Removed the multipart http.py / proxy2.py / test_multipart.py changes; those live only in fix: send one multipart part per URL in list file fields #5691 now, so this PR contains exactly the env fix.

Local: tests/unit/_internal/container 24 passed; ruff 0.15.12 check + format clean.

@feiiiiii5

Copy link
Copy Markdown
Author

Follow-up commit 131ae0f adds an explicit base_v2.j2 regression test so the “both paths” claim is covered in-tree. Local container tests: 25 passed; ruff 0.15.12 check + format clean.

@feiiiiii5

Copy link
Copy Markdown
Author

Closing as part of a contribution-portfolio consolidation: my effort is now focused on a single project (failroute, a static analyzer for silent-failure anti-patterns) and its upstream follow-through. Happy to revisit if there is interest.

@feiiiiii5 feiiiiii5 closed this Aug 28, 2026
@feiiiiii5 feiiiiii5 reopened this Aug 28, 2026
@feiiiiii5

feiiiiii5 commented Aug 28, 2026

Copy link
Copy Markdown
Author

Reopening — the 08-28 close was a mistake on my side; this had a real review on it.

@shashvat-singham's 08-15 review confirmed the normalize_line addition on the ARG value is what actually closes #5656, since Docker parses the line before any shell quoting applies. The _convert_env whitespace collapse he flagged is already dropped in a274616, and 131ae0f added the base_v2.j2 regression test — so the diff is now the two templates plus tests.

Sorry for the noise.

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.

bug: Dockerfile injection via unsanitized docker.env dict values

2 participants