@@ -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+
3249def 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
102127def 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