From 78a687c6decc869c7ae123f13c8e6132fe241597 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 04:16:39 +0000 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fix=20command=20injection=20in=20Makefile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Identified a critical command injection vulnerability in the Makefile's `project` target. - Replaced vulnerable shell-based logic with a secure Python script `project_init.py`. - Added input validation to prevent directory traversal and injection via the `NAME` variable. - Improved cross-platform compatibility by removing BSD-specific `sed -i ''` syntax. - Added Sentinel security journal entry in `.jules/sentinel.md`. --- .jules/sentinel.md | 6 +++++ Makefile | 14 +--------- project_init.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 .jules/sentinel.md create mode 100644 project_init.py diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..eef683a --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,6 @@ +# Sentinel Journal 🛡️ + +## 2025-05-08 - Command Injection in Makefile Project Initialization +**Vulnerability:** Command injection via the `NAME` variable in the `Makefile`. The `SOURCE` variable was defined using `$(shell echo ${NAME} | ... )`, which allows arbitrary command execution if `NAME` contains shell metacharacters or Make $(shell) syntax. Additionally, `sed` commands were using these variables unquoted. +**Learning:** Makefiles are often overlooked as a vector for command injection. When `$(shell ...)` or shell commands in targets use unquoted variables that can be overridden by the user, it creates a significant security risk. +**Prevention:** Avoid complex string manipulation and file renaming logic directly in the `Makefile` using shell commands when user input is involved. Use a dedicated script (like Python) with proper input validation and safe API calls to perform these tasks. diff --git a/Makefile b/Makefile index 27718a0..464011a 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,6 @@ DESCRIPTION ?= Python Project Template AUTHOR ?= Amr Abed EMAIL ?= amrabed GITHUB ?= amrabed -SOURCE ?= $(shell echo ${NAME} | tr '-' '_' | tr '[:upper:]' '[:lower:]') .PHONY: help help: # Show help @@ -13,18 +12,7 @@ help: # Show help .PHONY: project project: # Rename project (run once) - @if [ -d project ]; then mv project ${SOURCE}; fi - @sed -i '' 's/^::: project\.app/::: ${SOURCE}\.app/' docs/reference/app.md - @sed -i '' 's/^repo_name: .*/repo_name: ${GITHUB}\/${NAME}/' mkdocs.yml - @sed -i '' 's/^repo_url: .*/repo_url: https:\/\/github.com\/${GITHUB}\/${NAME}/' mkdocs.yml - @sed -i '' 's/^source = \[.*\]/source = \["${SOURCE}"\]/' pyproject.toml - @sed -i '' 's/^app = "project\.app:main"/app = "${SOURCE}\.app:main"/' pyproject.toml - @sed -i '' 's/^name = ".*"/name = "${SOURCE}"/' pyproject.toml - @sed -i '' 's/^description = ".*"/description = "${DESCRIPTION}"/' pyproject.toml - @sed -i '' 's/^authors = \[.*\]/authors = \["${AUTHOR} <${EMAIL}>"\]/' pyproject.toml - @sed -i '' 's/^# .*/# ${DESCRIPTION}/' docs/README.md - @sed -i '' 's/@.*/@${GITHUB}/' .github/CODEOWNERS - @sed -i '' 's/^github: \[.*\]/github: \[${GITHUB}\]/' .github/FUNDING.yml + @python3 project_init.py '$(subst ','\'',$(NAME))' '$(subst ','\'',$(DESCRIPTION))' '$(subst ','\'',$(AUTHOR))' '$(subst ','\'',$(EMAIL))' '$(subst ','\'',$(GITHUB))' uv: # Install uv pipx install -f uv diff --git a/project_init.py b/project_init.py new file mode 100644 index 0000000..f202cc1 --- /dev/null +++ b/project_init.py @@ -0,0 +1,67 @@ +import os +import re +import shutil +import sys +from pathlib import Path + + +def main(): + if len(sys.argv) != 6: + print("Usage: python3 project_init.py ") + sys.exit(1) + + name = sys.argv[1] + description = sys.argv[2] + author = sys.argv[3] + email = sys.argv[4] + github = sys.argv[5] + + # Validate name to prevent directory traversal or other injection + if not re.match(r"^[a-zA-Z0-9_-]+$", name): + print( + f"Error: Invalid project name '{name}'. Only alphanumeric characters, dashes, and underscores are allowed." + ) + sys.exit(1) + + source = name.replace("-", "_").lower() + + print(f"Initializing project '{name}' (source: '{source}')...") + + # 1. Rename project directory + if os.path.isdir("project"): + shutil.move("project", source) + elif not os.path.isdir(source): + print(f"Error: Neither 'project' nor '{source}' directory found.") + sys.exit(1) + + # 2. File modifications + replacements = [ + ("docs/reference/app.md", r"^::: project\.app", f"::: {source}.app"), + ("mkdocs.yml", r"^repo_name: .*", f"repo_name: {github}/{name}"), + ("mkdocs.yml", r"^repo_url: .*", f"repo_url: https://github.com/{github}/{name}"), + ("pyproject.toml", r"^source = \[.*\]", f'source = ["{source}"]'), + ("pyproject.toml", r'^app = "project\.app:main"', f'app = "{source}.app:main"'), + ("pyproject.toml", r'^name = ".*"', f'name = "{source}"'), + ("pyproject.toml", r'^description = ".*"', f'description = "{description}"'), + ("pyproject.toml", r"^authors = \[.*\]", f'authors = ["{author} <{email}>"]'), + ("docs/README.md", r"^# .*", f"# {description}"), + (".github/CODEOWNERS", r"@.*", f"@{github}"), + (".github/FUNDING.yml", r"^github: \[.*\]", f"github: [{github}]"), + ] + + for filepath, pattern, replacement in replacements: + path = Path(filepath) + if not path.exists(): + print(f"Warning: File {filepath} not found, skipping.") + continue + + content = path.read_text() + new_content = re.sub(pattern, replacement, content, flags=re.MULTILINE) + path.write_text(new_content) + print(f"Updated {filepath}") + + print("Project initialization complete.") + + +if __name__ == "__main__": + main() From 8d6350db10fccc72278cd7edfc63d87c1efe88a6 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 09:59:14 +0000 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Fix=20c?= =?UTF-8?q?ommand=20injection=20in=20Makefile=20project=20initialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Moved project initialization logic to `scripts/rename.py` with input validation. - Updated Makefile to use `uv run scripts/rename.py`. - Prevented command injection via shell-escaped parameters. - Added .[jJ]ules to .gitignore and removed sentinel journal. --- .gitignore | 1 + .jules/sentinel.md | 6 ------ Makefile | 2 +- project_init.py => scripts/rename.py | 0 4 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 .jules/sentinel.md rename project_init.py => scripts/rename.py (100%) diff --git a/.gitignore b/.gitignore index e261124..8eb1ad4 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,4 @@ cython_debug/ # PyPI configuration file .pypirc +.[jJ]ules diff --git a/.jules/sentinel.md b/.jules/sentinel.md deleted file mode 100644 index eef683a..0000000 --- a/.jules/sentinel.md +++ /dev/null @@ -1,6 +0,0 @@ -# Sentinel Journal 🛡️ - -## 2025-05-08 - Command Injection in Makefile Project Initialization -**Vulnerability:** Command injection via the `NAME` variable in the `Makefile`. The `SOURCE` variable was defined using `$(shell echo ${NAME} | ... )`, which allows arbitrary command execution if `NAME` contains shell metacharacters or Make $(shell) syntax. Additionally, `sed` commands were using these variables unquoted. -**Learning:** Makefiles are often overlooked as a vector for command injection. When `$(shell ...)` or shell commands in targets use unquoted variables that can be overridden by the user, it creates a significant security risk. -**Prevention:** Avoid complex string manipulation and file renaming logic directly in the `Makefile` using shell commands when user input is involved. Use a dedicated script (like Python) with proper input validation and safe API calls to perform these tasks. diff --git a/Makefile b/Makefile index 464011a..1cee315 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ help: # Show help .PHONY: project project: # Rename project (run once) - @python3 project_init.py '$(subst ','\'',$(NAME))' '$(subst ','\'',$(DESCRIPTION))' '$(subst ','\'',$(AUTHOR))' '$(subst ','\'',$(EMAIL))' '$(subst ','\'',$(GITHUB))' + @uv run scripts/rename.py '$(subst ','\'',$(NAME))' '$(subst ','\'',$(DESCRIPTION))' '$(subst ','\'',$(AUTHOR))' '$(subst ','\'',$(EMAIL))' '$(subst ','\'',$(GITHUB))' uv: # Install uv pipx install -f uv diff --git a/project_init.py b/scripts/rename.py similarity index 100% rename from project_init.py rename to scripts/rename.py From c2bcfe0acc2771344ebcda6827cbbdc1dd97c271 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 10:05:06 +0000 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Refacto?= =?UTF-8?q?r=20rename=20script=20to=20use=20click=20and=20named=20argument?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated `scripts/rename.py` to use the `click` library for robust argument parsing. - Refactored Makefile to pass project initialization parameters as named options. - Maintained security fixes and input validation. --- Makefile | 7 ++++++- scripts/rename.py | 35 +++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index 1cee315..b83713e 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,12 @@ help: # Show help .PHONY: project project: # Rename project (run once) - @uv run scripts/rename.py '$(subst ','\'',$(NAME))' '$(subst ','\'',$(DESCRIPTION))' '$(subst ','\'',$(AUTHOR))' '$(subst ','\'',$(EMAIL))' '$(subst ','\'',$(GITHUB))' + @uv run scripts/rename.py \ + --name '$(subst ','\'',$(NAME))' \ + --description '$(subst ','\'',$(DESCRIPTION))' \ + --author '$(subst ','\'',$(AUTHOR))' \ + --email '$(subst ','\'',$(EMAIL))' \ + --github '$(subst ','\'',$(GITHUB))' uv: # Install uv pipx install -f uv diff --git a/scripts/rename.py b/scripts/rename.py index f202cc1..53f19e8 100644 --- a/scripts/rename.py +++ b/scripts/rename.py @@ -1,38 +1,33 @@ import os import re import shutil -import sys from pathlib import Path +import click -def main(): - if len(sys.argv) != 6: - print("Usage: python3 project_init.py ") - sys.exit(1) - - name = sys.argv[1] - description = sys.argv[2] - author = sys.argv[3] - email = sys.argv[4] - github = sys.argv[5] +@click.command() +@click.option("--name", required=True, help="Project new name") +@click.option("--description", required=True, help="Project short description") +@click.option("--author", required=True, help="Author name") +@click.option("--email", required=True, help="Author email") +@click.option("--github", required=True, help="GitHub username") +def main(name: str, description: str, author: str, email: str, github: str): # Validate name to prevent directory traversal or other injection if not re.match(r"^[a-zA-Z0-9_-]+$", name): - print( - f"Error: Invalid project name '{name}'. Only alphanumeric characters, dashes, and underscores are allowed." + raise click.UsageError( + f"Invalid project name '{name}'. Only alphanumeric characters, dashes, and underscores are allowed." ) - sys.exit(1) source = name.replace("-", "_").lower() - print(f"Initializing project '{name}' (source: '{source}')...") + click.echo(f"Initializing project '{name}' (source: '{source}')...") # 1. Rename project directory if os.path.isdir("project"): shutil.move("project", source) elif not os.path.isdir(source): - print(f"Error: Neither 'project' nor '{source}' directory found.") - sys.exit(1) + raise click.ClickException(f"Error: Neither 'project' nor '{source}' directory found.") # 2. File modifications replacements = [ @@ -52,15 +47,15 @@ def main(): for filepath, pattern, replacement in replacements: path = Path(filepath) if not path.exists(): - print(f"Warning: File {filepath} not found, skipping.") + click.echo(f"Warning: File {filepath} not found, skipping.") continue content = path.read_text() new_content = re.sub(pattern, replacement, content, flags=re.MULTILINE) path.write_text(new_content) - print(f"Updated {filepath}") + click.echo(f"Updated {filepath}") - print("Project initialization complete.") + click.echo("Project initialization complete.") if __name__ == "__main__": From 39a2046a792c7624d26db8c939af087f83fd867f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 10:11:56 +0000 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Registe?= =?UTF-8?q?r=20rename=20script=20and=20improve=20gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Registered `rename` script in `pyproject.toml`. - Updated Makefile to use `uv run rename` with named arguments. - Added documentation comment for AI agent logs in `.gitignore`. --- .gitignore | 2 ++ Makefile | 2 +- pyproject.toml | 1 + scripts/__init__.py | 0 4 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 scripts/__init__.py diff --git a/.gitignore b/.gitignore index 8eb1ad4..132ce2c 100644 --- a/.gitignore +++ b/.gitignore @@ -170,4 +170,6 @@ cython_debug/ # PyPI configuration file .pypirc + +# AI agent logs .[jJ]ules diff --git a/Makefile b/Makefile index b83713e..d7486b2 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ help: # Show help .PHONY: project project: # Rename project (run once) - @uv run scripts/rename.py \ + @uv run rename \ --name '$(subst ','\'',$(NAME))' \ --description '$(subst ','\'',$(DESCRIPTION))' \ --author '$(subst ','\'',$(AUTHOR))' \ diff --git a/pyproject.toml b/pyproject.toml index 7b1fdf4..d59bc4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ [project.scripts] app = "project.app:main" +rename = "scripts.rename:main" [dependency-groups] dev = [ diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 From ab329bac3ba7ee45bd4113a880350d4fecb6393f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 10:15:36 +0000 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Require?= =?UTF-8?q?=20uv=20for=20project=20target=20and=20conditional=20install?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added `uv` as a prerequisite for the `project` target. - Updated the `uv` target to only install via `pipx` if not already present. - Improved Makefile robustness. --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d7486b2..1c8d0a3 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ help: # Show help @grep -E '^[a-zA-Z_-]+:.*?# .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?# "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' .PHONY: project -project: # Rename project (run once) +project: uv # Rename project (run once) @uv run rename \ --name '$(subst ','\'',$(NAME))' \ --description '$(subst ','\'',$(DESCRIPTION))' \ @@ -20,7 +20,7 @@ project: # Rename project (run once) --github '$(subst ','\'',$(GITHUB))' uv: # Install uv - pipx install -f uv + @command -v uv >/dev/null 2>&1 || pipx install uv venv: # Create and activate virtual environment and install dependencies uv sync From 04ac45112cb86b994c5a1763a9b9161246e83e89 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 10:18:49 +0000 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Update?= =?UTF-8?q?=20click=20import=20style=20in=20rename=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Changed click imports to `from click import ...` style. - Ensured code remains compliant with line length and linting rules. --- scripts/rename.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/scripts/rename.py b/scripts/rename.py index 53f19e8..4388ac2 100644 --- a/scripts/rename.py +++ b/scripts/rename.py @@ -3,31 +3,31 @@ import shutil from pathlib import Path -import click +from click import ClickException, UsageError, command, echo, option -@click.command() -@click.option("--name", required=True, help="Project new name") -@click.option("--description", required=True, help="Project short description") -@click.option("--author", required=True, help="Author name") -@click.option("--email", required=True, help="Author email") -@click.option("--github", required=True, help="GitHub username") +@command() +@option("--name", required=True, help="Project new name") +@option("--description", required=True, help="Project short description") +@option("--author", required=True, help="Author name") +@option("--email", required=True, help="Author email") +@option("--github", required=True, help="GitHub username") def main(name: str, description: str, author: str, email: str, github: str): # Validate name to prevent directory traversal or other injection if not re.match(r"^[a-zA-Z0-9_-]+$", name): - raise click.UsageError( + raise UsageError( f"Invalid project name '{name}'. Only alphanumeric characters, dashes, and underscores are allowed." ) source = name.replace("-", "_").lower() - click.echo(f"Initializing project '{name}' (source: '{source}')...") + echo(f"Initializing project '{name}' (source: '{source}')...") # 1. Rename project directory if os.path.isdir("project"): shutil.move("project", source) elif not os.path.isdir(source): - raise click.ClickException(f"Error: Neither 'project' nor '{source}' directory found.") + raise ClickException(f"Error: Neither 'project' nor '{source}' directory found.") # 2. File modifications replacements = [ @@ -47,15 +47,15 @@ def main(name: str, description: str, author: str, email: str, github: str): for filepath, pattern, replacement in replacements: path = Path(filepath) if not path.exists(): - click.echo(f"Warning: File {filepath} not found, skipping.") + echo(f"Warning: File {filepath} not found, skipping.") continue content = path.read_text() new_content = re.sub(pattern, replacement, content, flags=re.MULTILINE) path.write_text(new_content) - click.echo(f"Updated {filepath}") + echo(f"Updated {filepath}") - click.echo("Project initialization complete.") + echo("Project initialization complete.") if __name__ == "__main__":