2626logger = logging .getLogger (__name__ )
2727
2828
29- def _normalize_path_config (path_config : Any , label : str = "unknown" ) -> list [dict [str , str | None ]]:
30- """Normalize a path value (string or list-of-dicts) to canonical list format.
31-
32- Handles:
33- - ``str``: pre-migration plain string path (backward compat)
34- - ``list[dict]``: canonical v2.2+ format with ``match`` and optional ``rewrite``
35- """
36- if isinstance (path_config , str ):
37- logger .debug ("Found single path '%s' for '%s'" , path_config , label )
38- return [{"match" : path_config , "rewrite" : None }]
39-
40- if isinstance (path_config , list ):
41- result = []
42- for p in path_config :
43- if isinstance (p , dict ):
44- result .append ({"match" : p .get ("match" , "/" ), "rewrite" : p .get ("rewrite" )})
45- else :
46- result .append ({"match" : str (p ), "rewrite" : None })
47- if result :
48- logger .debug ("Found %d path(s) for '%s'" , len (result ), label )
49- return result
50-
51- logger .debug ("No path found for '%s', using default '/'" , label )
52- return [{"match" : "/" , "rewrite" : None }]
53-
54-
5529# Default resource values for deployment containers
5630DEFAULT_RESOURCES : dict [str , str ] = {
5731 "requests_memory" : "128Mi" ,
@@ -89,6 +63,53 @@ def _parse_resources_block(raw: dict | None, defaults: dict[str, str] | None = N
8963 }
9064
9165
66+ # URL-path charset voor tenant-bestuurde component match/rewrite waarden;
67+ # voorkomt nginx-snippet-injection in ingress.yaml.jinja.
68+ _SAFE_PATH_PATTERN = re .compile (r"^/[A-Za-z0-9/_.~-]*$" )
69+
70+
71+ def _sanitize_path_value (value : Any , field : str ) -> str :
72+ """
73+ Validate a tenant-supplied URL path used for ingress routing/rewriting.
74+
75+ Raises:
76+ ValueError: If the value is not a string or contains characters
77+ outside the safe URL-path charset (defense against nginx
78+ configuration-snippet injection).
79+ """
80+ if not isinstance (value , str ):
81+ raise ValueError ( # noqa: TRY004
82+ f"Component path '{ field } ' must be a string, got { type (value ).__name__ } "
83+ )
84+ if not value :
85+ raise ValueError (f"Component path '{ field } ' must not be empty" )
86+ if not _SAFE_PATH_PATTERN .match (value ):
87+ raise ValueError (
88+ f"Component path '{ field } ' contains illegal characters: { value !r} . "
89+ f"Only '/', letters, digits and '-._~' are allowed (no whitespace, "
90+ f"quotes or newlines)."
91+ )
92+ return value
93+
94+
95+ def _normalize_path_config (raw : Any , default_match : str = "/" ) -> dict [str , str | None ]:
96+ """
97+ Build a validated {"match": ..., "rewrite": ...} entry from a raw path item.
98+
99+ Both `match` and `rewrite` are sanitized against the safe URL-path charset.
100+ """
101+ if isinstance (raw , dict ):
102+ match_value = raw .get ("match" , default_match )
103+ rewrite_value = raw .get ("rewrite" )
104+ else :
105+ match_value = raw
106+ rewrite_value = None
107+
108+ sanitized_match = _sanitize_path_value (match_value , "match" )
109+ sanitized_rewrite = None if rewrite_value is None else _sanitize_path_value (rewrite_value , "rewrite" )
110+ return {"match" : sanitized_match , "rewrite" : sanitized_rewrite }
111+
112+
92113def _parse_resources_block_partial (raw : dict | None ) -> dict [str , str ]:
93114 """
94115 Parse a nested resources block into flat keys without filling defaults.
@@ -665,7 +686,18 @@ def extract_component_paths(self, project_data: dict[str, Any], component_name:
665686 """
666687 json_path = f"$.components[?(@.name='{ component_name } ')].path"
667688 path_config = self .extract_value_by_path (project_data , json_path , "/" )
668- return _normalize_path_config (path_config , component_name )
689+
690+ # Normaliseer naar list-format; sanitizer valideert per item.
691+ if isinstance (path_config , str ):
692+ logger .debug (f"Found single path '{ path_config } ' for component '{ component_name } '" )
693+ return [_normalize_path_config (path_config )]
694+ if isinstance (path_config , list ):
695+ result = [_normalize_path_config (p ) for p in path_config ]
696+ logger .info (f"Found { len (result )} path(s) for component '{ component_name } '" )
697+ return result
698+
699+ logger .debug (f"No path found for component '{ component_name } ', using default '/'" )
700+ return [{"match" : "/" , "rewrite" : None }]
669701
670702 def extract_deployment_component_paths (
671703 self ,
@@ -691,10 +723,16 @@ def extract_deployment_component_paths(
691723 components = deployment_data .get ("components" , [])
692724 for comp in components :
693725 if comp .get ("reference" ) == component_reference :
726+ # Schema: singular `path` (str | list); normaliseer via sanitizer.
694727 path_config = comp .get ("path" )
695728 if path_config is not None :
696- result = _normalize_path_config (path_config , component_reference )
697- if result != [{"match" : "/" , "rewrite" : None }]:
729+ if isinstance (path_config , str ):
730+ result = [_normalize_path_config (path_config )]
731+ elif isinstance (path_config , list ):
732+ result = [_normalize_path_config (p ) for p in path_config ]
733+ else :
734+ result = [{"match" : "/" , "rewrite" : None }]
735+ if result and result != [{"match" : "/" , "rewrite" : None }]:
698736 logger .info (
699737 f"Found { len (result )} deployment-level path(s) for component '{ component_reference } '"
700738 )
0 commit comments