Skip to content

Commit b536e52

Browse files
committed
refactor: enhance validation for identifiers and hostnames in topology JSON schema
1 parent d3e7530 commit b536e52

1 file changed

Lines changed: 37 additions & 4 deletions

File tree

manager_scripts/generate_exec/validation.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,23 @@ def require_non_empty_string(entry, key, context):
2929
return value
3030

3131

32+
def _is_valid_identifier(value):
33+
"""Check if value is a valid ROS-compatible identifier.
34+
35+
Allows alphanumerics, underscores, and hyphens; no spaces or shell metacharacters.
36+
"""
37+
import re
38+
return bool(re.match(r"^[A-Za-z0-9_-]+$", value))
39+
40+
41+
def _is_valid_host_name(value):
42+
"""Check if value is a safe hostname (subset of identifier).
43+
44+
Allows alphanumerics, underscores, and hyphens; prevents path traversal and shell injection.
45+
"""
46+
return _is_valid_identifier(value)
47+
48+
3249
def ensure_only_allowed_keys(entry, allowed_keys, context):
3350
"""Reject unknown keys to catch topology JSON typos early."""
3451
unknown_keys = sorted(set(entry.keys()) - set(allowed_keys))
@@ -81,7 +98,11 @@ def validate_publisher_entries(pub_entries, context):
8198
{"topic_name", "payload_size", "period_ms"},
8299
pub_context,
83100
)
84-
require_non_empty_string(pub, "topic_name", pub_context)
101+
topic_name_str = require_non_empty_string(pub, "topic_name", pub_context)
102+
if not _is_valid_identifier(topic_name_str):
103+
raise ValueError(
104+
f"{pub_context}: 'topic_name' must be a valid ROS identifier (alphanumerics, underscores, hyphens; no spaces or special characters)"
105+
)
85106
require_positive_int(pub, "payload_size", pub_context)
86107
require_positive_int(pub, "period_ms", pub_context)
87108

@@ -96,7 +117,11 @@ def validate_subscriber_entries(sub_entries, context):
96117
if not isinstance(sub, dict):
97118
raise ValueError(f"{sub_context}: must be an object")
98119
ensure_only_allowed_keys(sub, {"topic_name"}, sub_context)
99-
require_non_empty_string(sub, "topic_name", sub_context)
120+
topic_name_str = require_non_empty_string(sub, "topic_name", sub_context)
121+
if not _is_valid_identifier(topic_name_str):
122+
raise ValueError(
123+
f"{sub_context}: 'topic_name' must be a valid ROS identifier (alphanumerics, underscores, hyphens; no spaces or special characters)"
124+
)
100125

101126

102127
def normalize_intermediate_entries(intermediate_value, node_name):
@@ -146,7 +171,11 @@ def validate_topology_json_schema(json_content):
146171
if not isinstance(host, dict):
147172
raise ValueError(f"{host_context}: must be an object")
148173
ensure_only_allowed_keys(host, {"host_name", "nodes"}, host_context)
149-
require_non_empty_string(host, "host_name", host_context)
174+
host_name_str = require_non_empty_string(host, "host_name", host_context)
175+
if not _is_valid_host_name(host_name_str):
176+
raise ValueError(
177+
f"{host_context}: 'host_name' must contain only alphanumerics, underscores, and hyphens (no spaces, slashes, or special characters)"
178+
)
150179

151180
if "nodes" not in host:
152181
raise ValueError(f"{host_context}: 'nodes' is required")
@@ -165,7 +194,11 @@ def validate_topology_json_schema(json_content):
165194
{"node_name", "publisher", "subscriber", "intermediate"},
166195
node_context,
167196
)
168-
require_non_empty_string(node, "node_name", node_context)
197+
node_name_str = require_non_empty_string(node, "node_name", node_context)
198+
if not _is_valid_identifier(node_name_str):
199+
raise ValueError(
200+
f"{node_context}: 'node_name' must be a valid ROS identifier (alphanumerics, underscores, hyphens; no spaces or special characters)"
201+
)
169202

170203
has_role = any(
171204
role in node for role in ("publisher", "subscriber", "intermediate")

0 commit comments

Comments
 (0)