From 8ce421745f200b4d67a973d8f2c56781aba85372 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:33:31 -0700 Subject: [PATCH 1/2] Reject non-list security_opt in service definitions The Compose Specification defines security_opt as a sequence, and docker compose rejects a bare scalar with "services..security_opt must be a array". podman-compose instead wrapped a plain string into a one-element list alongside env_file and volumes, so a mistyped security_opt: label=disable was silently accepted while the equivalent docker compose file fails. Drop security_opt from the string-to-list coercion and raise PodmanComposeError when the value is not a list. Closes #1444 --- podman_compose.py | 4 +++- tests/unit/test_normalize_service.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/podman_compose.py b/podman_compose.py index fb8d31aa..8f578e62 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -2099,13 +2099,15 @@ def normalize_service(service: dict[str, Any], sub_dir: str = "") -> dict[str, A if "build" in service and "args" in service["build"]: if isinstance(build["args"], dict): build["args"] = norm_as_list(build["args"]) - for key in ("env_file", "security_opt", "volumes"): + for key in ("env_file", "volumes"): if key not in service: continue if isinstance(service[key], str): service[key] = [service[key]] if "security_opt" in service: sec_ls = service["security_opt"] + if not is_list(sec_ls): + raise PodmanComposeError("ERROR: security_opt must be a list") for ix, item in enumerate(sec_ls): if item in ("seccomp:unconfined", "apparmor:unconfined"): sec_ls[ix] = item.replace(":", "=") diff --git a/tests/unit/test_normalize_service.py b/tests/unit/test_normalize_service.py index 9907c84b..48aec98b 100644 --- a/tests/unit/test_normalize_service.py +++ b/tests/unit/test_normalize_service.py @@ -5,6 +5,7 @@ from parameterized import parameterized +from podman_compose import PodmanComposeError from podman_compose import normalize_service @@ -117,3 +118,17 @@ def test_command_like( expected_service = {} expected_service[key] = expected self.assertEqual(normalize_service(input_service), expected_service) + + @parameterized.expand([ + ("label=disable",), + ({"label": "disable"},), + ]) + def test_security_opt_must_be_a_list(self, value: Any) -> None: + with self.assertRaises(PodmanComposeError): + normalize_service({"security_opt": value}) + + def test_security_opt_list_is_normalized(self) -> None: + self.assertEqual( + normalize_service({"security_opt": ["seccomp:unconfined", "label=disable"]}), + {"security_opt": ["seccomp=unconfined", "label=disable"]}, + ) From 19da98ba176c679c982fdc6eed62907bfe8fe568 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:52:45 -0700 Subject: [PATCH 2/2] Add release note for security_opt validation --- newsfragments/reject_non_list_security_opt.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 newsfragments/reject_non_list_security_opt.bugfix diff --git a/newsfragments/reject_non_list_security_opt.bugfix b/newsfragments/reject_non_list_security_opt.bugfix new file mode 100644 index 00000000..86263e48 --- /dev/null +++ b/newsfragments/reject_non_list_security_opt.bugfix @@ -0,0 +1,2 @@ +Reject a non-list ``security_opt`` in service definitions instead of silently +coercing a bare scalar into a one-element list.