Repository: pydicom/pydicom

Issue:
Check that there are no eval() calls on DICOM data element values that would allow for remote code execution
Not sure if this is the right type of issue but ...

See [Tic TAC - Beware of your imaging](https://www.partywave.site/show/research/Tic%20TAC%20-%20Beware%20of%20your%20scan) description of a code execution exploit caused by use of eval() on the value of a DICOM data element ... not sure if any pydicom code would trigger this.

Hints from issue comments:
@dclunie: See also [CVE-2024-42845](https://nvd.nist.gov/vuln/detail/CVE-2024-42845).@mrbean-bremen: Thanks for the pointer!

That's indeed a weird implementation of `GetImagePosition`... 
While `pydicom` as a library does not (and should not) use `eval`, the pydicom cli interface does in one place. That looks benign to me, but it should be checked by somebody else. 
There is also `eval` used in the dump tool to evaluate the command line parameters, which looks a bit fishy to me. It could theoretically be exploited if somebody included the dump tool in an application without verifying the arguments - which seems a bit far-fetched.

Anyway, I do get a bit nervous seeing `eval` in any code, and I think it would make sense to replace it, even if the potential for an exploit is very low here. @darcymason - what do you think?
@darcymason: Thanks, @dclunie for pointing this out.  I had also seen this and was going to create an issue.

> not sure if any pydicom code would trigger this.

No, that `eval` was in a different library.

> That's indeed a weird implementation of GetImagePosition...

Yes, why did they not just use pydicom's built-in conversion?

> Anyway, I do get a bit nervous seeing eval in any code, and I think it would make sense to replace it, even if the potential for an exploit is very low here. @darcymason - what do you think?

The CLI eval() operates only on `ds.<rest-of-expression>` so although I'm nervous about it, I don't see how it could be exploited.  `<rest-of-expression>`'s characters are strongly limited by regex, and the test suite confirms nothing like parentheses (function calls) or assignment (= sign) can pass.  And it is a CLI tool, not mainline pydicom, so not something most people would use except with their own command-line arguments.  Still, happy for someone else to examine it very closely and make sure.

Regardless, perhaps the CLI docs and command-line messaging should be updated noting the (remote) possibility of `eval` issues if called by scripts or something.

The dump tool could probably be removed, honestly, but I'll take a look at making that safe.  Shouldn't be difficult.

BTW, there is also an `exec` in one test, which I think is completely unnecessary (just needs to `import` a  couple of constants) and I will make a PR to replace it.  It's only loading our own file, but _if_ somehow someone modified that file without us noticing (or modified it locally in someone's clone) then that could cause problems for anyone running the test suite.







@scaramallion: > Yes, why did they not just use pydicom's built-in conversion?

They're using GDCM's Python bindings.

>  It's only loading our own file, but if somehow someone modified that file without us noticing

There was a [scary exploit in xzutils](https://arstechnica.com/security/2024/04/what-we-know-about-the-xz-utils-backdoor-that-almost-infected-the-world/) where the payload was put in a test file.

Solution patch:
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index a72c8c820b..34d941a48d 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1 @@
-github: darcymason
\ No newline at end of file
+github: darcymason
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 1d69073bac..65ecee2b31 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -9,7 +9,7 @@ assignees: ''
 
 **Is your feature request related to a problem? Please describe.**
 A clear and concise description of what the problem is. Example: I'm always frustrated when [...]
-Please take into account that *pydicom* is a general-purpose framework and 
+Please take into account that *pydicom* is a general-purpose framework and
 does not handle the specifics of individual SOP classes.
 
 **Describe the solution you'd like**
diff --git a/.github/ISSUE_TEMPLATE/other_issues.md b/.github/ISSUE_TEMPLATE/other_issues.md
index ce61714d0c..5812bcf430 100644
--- a/.github/ISSUE_TEMPLATE/other_issues.md
+++ b/.github/ISSUE_TEMPLATE/other_issues.md
@@ -1,6 +1,6 @@
 ---
 name: Other issues
-about: For all other issues. Please consider creating a discussion item 
+about: For all other issues. Please consider creating a discussion item
 instead if you have a question, an idea, or a general discussion item.
 title: ''
 assignees: ''
diff --git a/.github/codecov.yml b/.github/codecov.yml
index 6407230ecd..914f860929 100644
--- a/.github/codecov.yml
+++ b/.github/codecov.yml
@@ -2,4 +2,4 @@ coverage:
   status:
     project:
       default:
-        threshold: 0.2%
\ No newline at end of file
+        threshold: 0.2%
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 5a4d8d75b3..73b72bd9c0 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -12,4 +12,4 @@ updates:
   - package-ecosystem: "github-actions"
     directory: "/"
     schedule:
-      interval: "monthly"
\ No newline at end of file
+      interval: "monthly"
diff --git a/.github/workflows/PR.yml b/.github/workflows/PR.yml
index a57a67b765..4adb548515 100644
--- a/.github/workflows/PR.yml
+++ b/.github/workflows/PR.yml
@@ -30,7 +30,7 @@ jobs:
             upload-coverage: false
           # Pre-release Python
           - os: ubuntu
-            python-version: "3.14"  
+            python-version: "3.14"
             test-numpy: false
             test-extras: false
             upload-coverage: false
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3e63630e87..3f57566d2a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,5 +1,8 @@
-# See https://pre-commit.com for more information
-# See https://pre-commit.com/hooks.html for more hooks
+exclude: test_files
+
+ci:
+  autoupdate_schedule: "monthly"
+
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
     rev: v4.5.0
@@ -13,26 +16,26 @@ repos:
       - id: end-of-file-fixer
       - id: mixed-line-ending
       - id: trailing-whitespace
-repos:
-- repo: https://github.com/crate-ci/typos
-  rev: v1.16.25
-  hooks:
-    - id: typos
-      exclude: _.*_dict.py
-- repo: https://github.com/astral-sh/ruff-pre-commit
-  # Ruff version.
-  rev: v0.5.1
-  hooks:
-  - id: ruff
-    args: [src, --fix, --show-fixes, --exit-non-zero-on-fix]
-- repo: https://github.com/psf/black-pre-commit-mirror
-  rev: 24.4.2
-  hooks:
-  - id: black
-- repo: https://github.com/pre-commit/mirrors-mypy
-  rev: v1.10.0
-  hooks:
-    - id: mypy
-      files: src
-      args: []
-      additional_dependencies: ["types-requests"]
+
+  - repo: https://github.com/crate-ci/typos
+    rev: v1.16.25
+    hooks:
+      - id: typos
+        exclude: _.*_dict.py
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.5.1
+    hooks:
+      - id: ruff
+        files: src
+        args: [--fix, --show-fixes, --exit-non-zero-on-fix]
+  - repo: https://github.com/psf/black-pre-commit-mirror
+    rev: 24.4.2
+    hooks:
+      - id: black
+  - repo: https://github.com/pre-commit/mirrors-mypy
+    rev: v1.10.0
+    hooks:
+      - id: mypy
+        files: src
+        args: []
+        additional_dependencies: ["types-requests"]
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2ab688ad36..656906cde5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -62,7 +62,7 @@ GitHub, clone, and develop on a branch. Steps:
    -  closes #1016
    ```
    ```
-   Add warning when saving compressed without encapsulation  
+   Add warning when saving compressed without encapsulation
    ```
    ```
    Add optional handler argument to Dataset.decompress()
diff --git a/LICENSE b/LICENSE
index 8d756f0fae..7070861786 100644
--- a/LICENSE
+++ b/LICENSE
@@ -22,7 +22,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 
-Portions of pydicom (private dictionary file(s)) were generated from the 
+Portions of pydicom (private dictionary file(s)) were generated from the
 private dictionary of the GDCM library, released under the following license:
 
   Program: GDCM (Grassroots DICOM). A DICOM library
diff --git a/_typos.toml b/_typos.toml
index ea9585d696..79a38e5ec6 100644
--- a/_typos.toml
+++ b/_typos.toml
@@ -8,4 +8,4 @@ womens = "womens"
 observ = "observ"
 
 [files]
-extend-exclude = ["**/_*_dict.py"]
\ No newline at end of file
+extend-exclude = ["**/_*_dict.py"]
diff --git a/doc/release_notes/v2.3.0.rst b/doc/release_notes/v2.3.0.rst
index 411ce31e9f..9ffa4cfa92 100644
--- a/doc/release_notes/v2.3.0.rst
+++ b/doc/release_notes/v2.3.0.rst
@@ -55,4 +55,3 @@ Fixes
   assigning the single value (:issue:`1606`)
 * Fixed an exception with multiple deferred reads with file-like objects
   (:issue:`1609`)
-
diff --git a/doc/release_notes/v3.1.0.rst b/doc/release_notes/v3.1.0.rst
index 3770cc0b7d..2b2c4abd44 100644
--- a/doc/release_notes/v3.1.0.rst
+++ b/doc/release_notes/v3.1.0.rst
@@ -40,3 +40,4 @@ Enhancements
     * :attr:`~pydicom.uid.JPEGXLJPEGRecompression`
     * :attr:`~pydicom.uid.JPEGXL`
 * Added ability to specify tag numbers in the CLI commands (allows private tags to be specified)
+* Removed `exec` and `eval` from tests, CLI, and scripts for improved security (:issue:`2193`)
diff --git a/pyproject.toml b/pyproject.toml
index d77dfe7b58..81f8f8530a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -53,7 +53,7 @@ dev = [
     "pydicom-data",
     "pytest",
     "pytest-cov",
-    "ruff==0.8.4",
+    "ruff==0.9.1",
     "types-requests",
     "pre-commit",
 ]
diff --git a/src/pydicom/cli/main.py b/src/pydicom/cli/main.py
index 8dcc8878bd..8f052706c9 100644
--- a/src/pydicom/cli/main.py
+++ b/src/pydicom/cli/main.py
@@ -16,7 +16,10 @@
 
 from pydicom import dcmread
 from pydicom.data.data_manager import get_charset_files, get_testdata_file
-from pydicom.dataset import Dataset
+from pydicom.datadict import tag_for_keyword
+from pydicom.dataset import Dataset, FileDataset
+from pydicom.uid import UID
+from pydicom.valuerep import PersonName
 
 
 subparsers: argparse._SubParsersAction | None = None
@@ -30,15 +33,45 @@
     r"|"  # or
     r"\([0-9A-Fa-f]{4},[0-9A-Fa-f]{4}\)"  # DICOM hex tag (gggg,eeee)
     r")"
-    r"(\[(-)?\d+\])?"  # Optional [index] or [-index]
+    r"(?:\[(-?\d+)\])?"  # Optional [index] or [-index], keep inside brackets
 )
 
 re_file_spec_object = re.compile(re_kywd_or_item + r"(\." + re_kywd_or_item + r")*$")
 
-re_tag_sub_from = r"\.\(([0-9A-Fa-f]{4}),([0-9A-Fa-f]{4})\)"
-re_tag_sub_to = r"[(0x\1,0x\2)].value"
+re_match_tag = r"\(([0-9A-Fa-f]{4}),([0-9A-Fa-f]{4})\)"
 re_tag_with_spaces = r"\([0-9A-Fa-f]{4}, +[0-9A-Fa-f]{4}\)"
 
+EXTRA_ALLOWED_IDENTIFIERS = {
+    # selected properties via {v[0] for v in inspect.getmembers(UID, inspect.isdatadescriptor)}
+    FileDataset: ("file_meta",),
+    Dataset: ("file_meta",),
+    UID: (
+        "keyword",
+        "is_compressed",
+        "info",
+        "is_encapsulated",
+        "is_retired",
+        "is_valid",
+        "is_little_endian",
+        "is_private",
+        "is_implicit_VR",
+        "name",
+        "is_transfer_syntax",
+        "is_deflated",
+    ),
+    PersonName: (
+        "family_name",
+        "ideographic",
+        "name_suffix",
+        "given_name",
+        "components",
+        "phonetic",
+        "middle_name",
+        "alphabetic",
+        "name_prefix",
+    ),
+}
+
 filespec_help = (
     "File specification, in format [pydicom::]filename[::element]. "
     "If `pydicom::` prefix is present, then use the pydicom "
@@ -55,20 +88,51 @@
 
 
 def eval_element(ds: Dataset, element: str) -> Any:
-    if element[0] != ".":
-        element = "." + element
-
     # replace all ".(gggg,eeee)" hex tags with `eval`uable expression
-    element = re.sub(re_tag_sub_from, re_tag_sub_to, element)
 
-    try:
-        return eval("ds" + element, {"ds": ds})
-    except AttributeError:
-        raise argparse.ArgumentTypeError(
-            f"Data element '{element}' is not in the dataset"
-        )
-    except IndexError as e:
-        raise argparse.ArgumentTypeError(f"'{element}' has an index error: {e}")
+    obj: Dataset | None = ds
+    for sub_elem in element.split("."):
+        # e.g. match "BeamSequence[1]" --> groups: ['BeamSequence', '1']
+        m = re.match(re_kywd_or_item, sub_elem)
+        identifier = m.groups()[0]  # type: ignore
+
+        if tag_for_keyword(identifier) is not None:  # Standard DICOM keyword
+            obj = getattr(obj, identifier, None)
+            if obj is None:
+                raise argparse.ArgumentTypeError(
+                    f"'{identifier}' is not in the parent object"
+                )
+        # Try (gggg,eee) tag
+        elif match := re.match(re_match_tag, identifier):
+            try:
+                obj = cast(Dataset, obj)["".join(match.groups()[0:2])].value
+            except KeyError:
+                raise argparse.ArgumentTypeError(
+                    f"'{identifier}' is not in the parent object"
+                )
+        # Try other identifiers
+        elif identifier in EXTRA_ALLOWED_IDENTIFIERS.get(type(obj), []):
+            obj = getattr(obj, identifier, None)
+            if obj is None:
+                raise argparse.ArgumentTypeError(
+                    f"'{identifier}' is not in the parent object"
+                )
+        else:
+            raise argparse.ArgumentTypeError(
+                f"'{identifier}' is not a known DICOM keyword, "
+                "tag or allowed class property"
+            )
+
+        # If here, then have the new object, handle indexing if there
+        if (index := m.groups()[1]) is not None:  # type: ignore
+            try:
+                obj = obj[int(index)]
+            except (IndexError, TypeError) as e:
+                raise argparse.ArgumentTypeError(
+                    f"'{index}' gave an index error: {str(e)}"
+                )
+
+    return obj
 
 
 def filespec_parts(filespec: str) -> tuple[str, str, str]:
diff --git a/src/pydicom/data/charset_files/FileInfo.txt b/src/pydicom/data/charset_files/FileInfo.txt
index 6282fbac65..3df0127181 100644
--- a/src/pydicom/data/charset_files/FileInfo.txt
+++ b/src/pydicom/data/charset_files/FileInfo.txt
@@ -20,4 +20,3 @@ chrSQEncoding.dcm is a minimal constructed dataset with a sequence that has
     another encoding (['ISO 2022 IR 13', 'ISO 2022 IR 87']) than the dataset (ISO_IR 192)
 chrSQEncoding1.dcm is the same dataset with the encoding ['ISO 2022 IR 13', 'ISO 2022 IR 87']
     defined in the dataset, but not in the sequence
-
diff --git a/src/pydicom/util/dump.py b/src/pydicom/util/dump.py
index 32ec2f87ac..00e5af0df7 100644
--- a/src/pydicom/util/dump.py
+++ b/src/pydicom/util/dump.py
@@ -1,4 +1,4 @@
-# Copyright 2008-2021 pydicom authors. See LICENSE file for details.
+# Copyright 2008-2025 pydicom authors. See LICENSE file for details.
 """Utility functions used in debugging writing and reading"""
 
 from io import BytesIO
@@ -128,8 +128,8 @@ def pretty_print(
     start_address = 0
     stop_address = None
     if len(sys.argv) > 2:  # then have start address
-        start_address = eval(sys.argv[2])
+        start_address = int(sys.argv[2], 16)
     if len(sys.argv) > 3:
-        stop_address = eval(sys.argv[3])
+        stop_address = int(sys.argv[3], 16)
 
     print(filedump(filename, start_address, stop_address))


Test patch:
diff --git a/tests/_write_stds.py b/tests/_write_stds.py
index ffd9f2e58b..c7939d9fe2 100644
--- a/tests/_write_stds.py
+++ b/tests/_write_stds.py
@@ -1,11 +1,8 @@
-# Copyright 2008-2018 pydicom authors. See LICENSE file for details.
+# Copyright 2008-2025 pydicom authors. See LICENSE file for details.
+"""Test data for a couple of tests"""
 
-"""Snippets for what a particular dataset (including nested sequences)
-should look like after writing in different expl/impl Vr and endian combos,
-as well as undefined length sequences and items
-"""
 
-# Implicit VR, little endian, SQ's with defined lengths
+# Test data: Implicit VR, little endian, SQ with defined lengths
 impl_LE_deflen_std_hex = (
     b"10 00 10 00 "  # (0010,0010) Patient's Name
     b"0c 00 00 00 "  # length 12
@@ -36,36 +33,3 @@
     b"33 32 5c 36 34 5c 31 32 38 5c 31 39 36 20 "
     # "32\64\128\196 "
 )
-
-# Implicit VR, big endian, SQ's with defined lengths
-# Realized after coding this that there is no Impl VR big endian in DICOM std;
-#    however, it seems to exist as a GE private transfer syntax.
-#    Will leave this here for now.
-impl_BE_deflen_std_hex = (
-    b"00 10 00 10 "  # (0010,0010) Patient's Name
-    b"00 00 00 0c "  # length 12
-    b"4e 61 6d 65 5e 50 61 74 69 65 6e 74 "  # "Name^Patient"
-    b"30 06 00 39 "  # (3006,0039) ROI Contour Sequence
-    b"00 00 00 5a "  # length 90
-    b"ff fe e0 00 "  # (FFFE,E000) Item Tag
-    b"00 00 00 52 "  # length 82
-    b"30 06 00 40 "  # (3006,0040)  Contour Sequence
-    b"00 00 00 4a "  # length 74
-    b"ff fe e0 00 "  # (FFFE,E000) Item Tag
-    b"00 00 00 1a "  # length 26
-    b"30 06 00 48 "  # (3006,0048) Contour Number
-    b"00 00 00 02 "  # length 2
-    b"31 20 "  # "1 "
-    b"30 06 00 50 "  # (3006,0050) Contour Data
-    b"00 00 00 08 "  # length 8
-    b"32 5c 34 5c 38 5c 31 36 "  # "2\4\8\16"
-    b"ff fe e0 00 "  # (FFFE,E000) Item Tag
-    b"20 00 00 00 "  # length 32
-    b"30 06 00 48 "  # (3006,0048) Contour Number
-    b"00 00 00 02 "  # length 2
-    b"32 20 "  # "2 "
-    b"30 06 00 50 "  # (3006,0050) Contour Data
-    b"00 00 00 0e "  # length 14
-    b"33 32 5c 36 34 5c 31 32 38 5c 31 39 36 20 "
-    # "32\64\128\196 "
-)
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 2dcb5d67dc..5de1dce915 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -21,12 +21,13 @@
     "BeamSequence[0].(10,10)",  # nested bad tag format
 )
 
-missing_elements = (
+not_dicom_keywords = (
     "NotThere",
     "BeamSequenceXX",
-    "BeamDose",  # valid keyword but not at top level
 )
 
+not_in_parent = ("BeamDose",)  # valid keyword but not at top level
+
 bad_indexes = (
     "BeamSequence[42]",
     "BeamSequence[-42]",
@@ -43,16 +44,25 @@ def test_syntax(self, bad_spec):
         with pytest.raises(ArgumentTypeError, match=match):
             filespec_parser(f"pydicom::rtplan.dcm::{bad_spec}")
 
-    @pytest.mark.parametrize("missing_element", missing_elements)
-    def test_elem_not_exists(self, missing_element):
+    @pytest.mark.parametrize("missing_element", not_dicom_keywords)
+    def test_elem_not_keyword(self, missing_element):
+        """CLI filespec elements not in the dataset raise an error"""
+        with pytest.raises(
+            ArgumentTypeError,
+            match=r".*not a known DICOM keyword, tag or allowed class property",
+        ):
+            filespec_parser(f"pydicom::rtplan.dcm::{missing_element}")
+
+    @pytest.mark.parametrize("missing_element", not_in_parent)
+    def test_elem_not_in_parent(self, missing_element):
         """CLI filespec elements not in the dataset raise an error"""
-        with pytest.raises(ArgumentTypeError, match=r".* is not in the dataset"):
+        with pytest.raises(ArgumentTypeError, match=r".*not in the parent object"):
             filespec_parser(f"pydicom::rtplan.dcm::{missing_element}")
 
     @pytest.mark.parametrize("bad_index", bad_indexes)
     def test_bad_index(self, bad_index):
         """CLI filespec elements with an invalid index raise an error"""
-        with pytest.raises(ArgumentTypeError, match=r".* index error"):
+        with pytest.raises(ArgumentTypeError, match=r".*index error"):
             filespec_parser(f"pydicom::rtplan.dcm::{bad_index}")
 
     def test_offers_pydicom_testfile(self):
@@ -90,6 +100,8 @@ class TestFilespecElementEval:
             ("BeamSequence[0].ControlPointSequence[0].NominalBeamEnergy", 6.0),
             ("PatientID", "id00001"),
             ("(0010,0020)", "id00001"),
+            ("file_meta.TransferSyntaxUID.name", "Implicit VR Little Endian"),
+            ("PatientName.family_name", "Last"),
         ),
     )
     def test_correct_data_elements(self, elem_str, expected):
diff --git a/tests/test_util.py b/tests/test_util.py
index 0d78c277ec..e3374d08fe 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -1,4 +1,4 @@
-# Copyright 2008-2021 pydicom authors. See LICENSE file for details.
+# Copyright 2008-2025 pydicom authors. See LICENSE file for details.
 """Test suite for util functions"""
 import copy
 from contextlib import contextmanager
@@ -39,6 +39,7 @@
 )
 from pydicom.util.hexutil import hex2bytes, bytes2hex
 from pydicom.util.leanread import dicomfile
+from ._write_stds import impl_LE_deflen_std_hex
 
 have_numpy = True
 try:
@@ -46,15 +47,10 @@
 except ImportError:
     have_numpy = False
 
-test_dir = os.path.dirname(__file__)
-raw_hex_module = os.path.join(test_dir, "_write_stds.py")
-raw_hex_code = open(raw_hex_module, "rb").read()
-
 
 # For Python >=3.11, this can be imported from contextlib
 @contextmanager
 def chdir(new_dir):
-    import os
 
     old_dir = os.getcwd()
     try:
@@ -377,9 +373,7 @@ def test_bytes_to_hex(self):
 class TestDataElementCallbackTests:
     def setup_method(self):
         # Set up a dataset with commas in one item instead of backslash
-        namespace = {}
-        exec(raw_hex_code, {}, namespace)
-        ds_bytes = hexutil.hex2bytes(namespace["impl_LE_deflen_std_hex"])
+        ds_bytes = hexutil.hex2bytes(impl_LE_deflen_std_hex)
         # Change "2\4\8\16" to "2,4,8,16"
         ds_bytes = ds_bytes.replace(
             b"\x32\x5c\x34\x5c\x38\x5c\x31\x36", b"\x32\x2c\x34\x2c\x38\x2c\x31\x36"