From 56aad8875f3a8def48c93b9e9804f6435f78bb18 Mon Sep 17 00:00:00 2001 From: Chris Clements Date: Wed, 16 Sep 2026 11:20:53 -0400 Subject: [PATCH 1/6] Update versions.json.jinja2 Add `tojson` to ensure it produces valid JSON for non-numeric versions. --- templates/versions.json.jinja2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/versions.json.jinja2 b/templates/versions.json.jinja2 index 35a1c5b..b198732 100644 --- a/templates/versions.json.jinja2 +++ b/templates/versions.json.jinja2 @@ -1,4 +1,4 @@ { - "current_version": {{ current_version }}, + "current_version": {{ current_version | tojson }}, "versions": {{ versions | reverse | list | tojson }} } From 51b169260789a6ab42622db23230e7848e7bb948 Mon Sep 17 00:00:00 2001 From: Chris Clements Date: Wed, 16 Sep 2026 12:06:29 -0400 Subject: [PATCH 2/6] Update generate.py Update the imports and include select_autoescape to automatically and safely escape template variables for .html and .xml files. --- src/generate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/generate.py b/src/generate.py index 8368319..a916493 100644 --- a/src/generate.py +++ b/src/generate.py @@ -11,8 +11,13 @@ import urllib.parse import urllib.request -from jinja2 import Environment, FileSystemLoader +import jinja2 +from jinja2 import Environment, FileSystemLoader, select_autoescape +env = Environment( + loader=FileSystemLoader(template_dir), + autoescape=select_autoescape(["html", "xml"]) +) class Filters: From 3bd9f5b73c037abc303fa390690b7ac931e7a4af Mon Sep 17 00:00:00 2001 From: Chris Clements Date: Wed, 16 Sep 2026 12:26:42 -0400 Subject: [PATCH 3/6] Update generate.py Where front-matter is parsed from the file content, validate that the parsed result is an instance of dict. If it is None or any non-dict type, safely default it to an empty dictionary ({}) or raise a clean, descriptive warning/error. --- src/generate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/generate.py b/src/generate.py index a916493..613dabb 100644 --- a/src/generate.py +++ b/src/generate.py @@ -159,6 +159,8 @@ def render_file(input_path, output_path, env, page_context={}): # Parse the front matter if match: front_matter = yaml.safe_load(match.group(1)) # Parse YAML + if not isinstance(front_matter, dict): + front_matter = {} md_content = match.group(2) # Extract Markdown part else: front_matter = {} From 2c3c077deb120b824ed08fae53bb48332ab7fcd8 Mon Sep 17 00:00:00 2001 From: Chris Clements Date: Wed, 16 Sep 2026 12:37:57 -0400 Subject: [PATCH 4/6] Update generate.py Prevent symlink traversal and host-file disclosure. --- src/generate.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/generate.py b/src/generate.py index 613dabb..901781c 100644 --- a/src/generate.py +++ b/src/generate.py @@ -218,6 +218,12 @@ def render_markdown(input_dir, output_dir, env, page_context={}) -> ConversionRe for root, _, files in os.walk(input_dir): for filename in files: input_path = os.path.join(root, filename) + + # Skip symbolic links to prevent arbitrary host-file disclosure (b/528741819) + if os.path.islink(input_path): + print(f"Warning: Skipping symbolic link: {input_path}") + continue + # Determine the relative path (directory structure under input_dir). relative_path = os.path.relpath(os.path.dirname(input_path), input_dir) From 77d233970052cef0061b8cb41318456236127492 Mon Sep 17 00:00:00 2001 From: Chris Clements Date: Wed, 16 Sep 2026 12:51:57 -0400 Subject: [PATCH 5/6] Update generate.py Implement `_require_within` as a path containment helper function and apply it to validate all config-controlled paths. --- src/generate.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/generate.py b/src/generate.py index 901781c..a0fc472 100644 --- a/src/generate.py +++ b/src/generate.py @@ -14,6 +14,13 @@ import jinja2 from jinja2 import Environment, FileSystemLoader, select_autoescape +def _require_within(base_dir: str, target_path: str, error_msg: str) -> str: + canonical_base = os.path.realpath(base_dir) + canonical_target = os.path.realpath(os.path.join(canonical_base, target_path)) + if not canonical_target.startswith(canonical_base + os.sep) and canonical_target != canonical_base: + raise ValueError(f"Security Violation: {error_msg} '{target_path}' escapes boundaries.") + return canonical_target + env = Environment( loader=FileSystemLoader(template_dir), autoescape=select_autoescape(["html", "xml"]) @@ -326,6 +333,12 @@ def main(): config.setdefault("output_dir", OUTPUT_DIR_DEFAULT) config.setdefault("context", CONTEXT_DEFAULT.copy()) + # Validate directory configuration containment (b/528741576) + repo_root = os.getcwd() + config["input_dir"] = _require_within(repo_root, config["input_dir"], "input_dir") + config["template_dir"] = _require_within(repo_root, config["template_dir"], "template_dir") + config["output_dir"] = _require_within(repo_root, config["output_dir"], "output_dir") + # Override directories from config root with CLI args, if given if args.input_dir: config["input_dir"] = args.input_dir @@ -376,7 +389,9 @@ def main(): if current_version and version.get("version", "") != current_version: continue print(version) - output_path = os.path.join(config.get("output_dir"), version["path"], "index.html") + # Validate that version["path"] stays strictly inside output_dir (b/528741576) + target_dir = _require_within(config["output_dir"], version["path"], "versions[].path") + output_path = os.path.join(target_dir, "index.html") output_policy = os.path.join(config.get("output_dir"), "crp", "policy", "index.html") print(f"Will copy {output_policy} to {output_path}") os.makedirs(os.path.dirname(output_path), exist_ok=True) From f005efeb060e20af32c6c794e0023eb88b6518cc Mon Sep 17 00:00:00 2001 From: Chris Clements Date: Wed, 16 Sep 2026 13:09:26 -0400 Subject: [PATCH 6/6] Fix NameError --- src/generate.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/generate.py b/src/generate.py index a0fc472..2a6178d 100644 --- a/src/generate.py +++ b/src/generate.py @@ -21,11 +21,6 @@ def _require_within(base_dir: str, target_path: str, error_msg: str) -> str: raise ValueError(f"Security Violation: {error_msg} '{target_path}' escapes boundaries.") return canonical_target -env = Environment( - loader=FileSystemLoader(template_dir), - autoescape=select_autoescape(["html", "xml"]) -) - class Filters: @classmethod @@ -353,7 +348,10 @@ def main(): config["context"][key] = value # Load Jinja2 templates - env = Environment(loader=FileSystemLoader(config["template_dir"])) + env = Environment( + loader=FileSystemLoader(config["template_dir"]), + autoescape=select_autoescape(["html", "xml"]), + ) env.filters["absolute_url"] = lambda x: Filters.absolute_url( config["context"]["base_url"], x )