fix(l4d2-web): normalize CRLF to LF in script overlay POST

HTML <textarea> form submission encodes line breaks as CRLF per spec.
Storing those CRLFs unchanged means every line of the script reaches
bash with a trailing \r, which bash treats as part of the argument —
turning "ls /" into "ls /\r" and failing. Normalize CRLF/CR → LF in the
/overlays/{id}/script handler so storage and the sandbox tmpfile are
LF-only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-08 16:20:10 +02:00
parent 908bca3687
commit a62f26ba4a
No known key found for this signature in database
2 changed files with 22 additions and 1 deletions

View file

@ -145,7 +145,11 @@ def _load_script_overlay(db, overlay_id: int, user) -> tuple[Overlay | None, Res
def update_script(overlay_id: int) -> Response: def update_script(overlay_id: int) -> Response:
user = current_user() user = current_user()
assert user is not None assert user is not None
script_text = request.form.get("script", "") # HTML form submission of <textarea> uses CRLF line endings per spec; bash
# treats the trailing \r as part of each argument and breaks every command.
# Normalize to LF before storage so the script is well-formed when written
# to the sandbox tmpfile.
script_text = request.form.get("script", "").replace("\r\n", "\n").replace("\r", "\n")
with session_scope() as db: with session_scope() as db:
overlay, err = _load_script_overlay(db, overlay_id, user) overlay, err = _load_script_overlay(db, overlay_id, user)
if err is not None: if err is not None:

View file

@ -139,6 +139,23 @@ def test_update_script_body_enqueues_build(app, alice_id) -> None:
assert len(jobs) == 1 assert len(jobs) == 1
def test_update_script_normalizes_crlf_to_lf(app, alice_id) -> None:
"""HTML <textarea> submits CRLF line endings; bash chokes on trailing \\r
in every command. Storage must be LF-only so the sandbox tmpfile is
well-formed."""
overlay_id = _create_script_overlay(app, alice_id)
client = _client_for(app, alice_id)
client.post(
f"/overlays/{overlay_id}/script",
data={"script": "ls /\r\necho hello\r\n"},
headers={"X-CSRF-Token": "test-token"},
)
with session_scope() as s:
overlay = s.query(Overlay).filter_by(id=overlay_id).one()
assert overlay.script == "ls /\necho hello\n"
assert "\r" not in overlay.script
def test_manual_rebuild(app, alice_id) -> None: def test_manual_rebuild(app, alice_id) -> None:
overlay_id = _create_script_overlay(app, alice_id) overlay_id = _create_script_overlay(app, alice_id)
client = _client_for(app, alice_id) client = _client_for(app, alice_id)