diff --git a/README.md b/README.md index ea5e881..506441d 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,6 @@ query { ``` **Set an alarm:** -*(Note: `command` is optional. If omitted, the API will automatically figure out the correct command to start `wecker.py`)* ```graphql mutation { setAlarm( @@ -91,6 +90,7 @@ mutation { } } ``` +*(Note: The `command` is managed automatically by the API and cannot be overridden, to prevent command injection into the system crontab.)* **Delete an alarm:** ```graphql diff --git a/api/schema.py b/api/schema.py index 976383b..0688dd9 100644 --- a/api/schema.py +++ b/api/schema.py @@ -70,12 +70,10 @@ class Mutation: def set_alarm( self, cron_expression: str, - command: Optional[str] = None, is_enabled: bool = True, id: Optional[str] = None, ) -> Alarm: - if command is None: - command = _default_command() + command = _default_command() manager = get_manager() new_id = manager.set_alarm( diff --git a/tests/test_api.py b/tests/test_api.py index e7b7b5f..80c7069 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -121,6 +121,22 @@ def test_set_alarm_default_command_append(): client.post("/graphql", json={"query": mutation_delete}, headers=headers) +def test_set_alarm_rejects_custom_command(): + """Custom command argument must be rejected to prevent crontab injection.""" + headers = {"X-API-Key": "test-secret"} + mutation = """ + mutation { + setAlarm(cronExpression: "0 9 * * *", command: "rm -rf /") { + id + } + } + """ + res = client.post("/graphql", json={"query": mutation}, headers=headers) + assert res.status_code == 200 + assert "errors" in res.json() + assert "command" in str(res.json()["errors"]) + + def test_graphql_workflow(): headers = {"X-API-Key": "test-secret"} @@ -139,7 +155,7 @@ def test_graphql_workflow(): # 2. Set alarm mutation_set = """ mutation { - setAlarm(cronExpression: "30 7 * * *", command: "python wecker.py", isEnabled: true) { + setAlarm(cronExpression: "30 7 * * *", isEnabled: true) { id cronExpression command @@ -151,7 +167,7 @@ def test_graphql_workflow(): assert res.status_code == 200 alarm = res.json()["data"]["setAlarm"] assert alarm["cronExpression"] == "30 7 * * *" - assert alarm["command"] == "python wecker.py" + assert "wecker.py" in alarm["command"] assert alarm["isEnabled"] is True alarm_id = alarm["id"] @@ -176,7 +192,7 @@ def test_graphql_workflow(): # 5. Update alarm mutation_update = f""" mutation {{ - setAlarm(id: "{alarm_id}", cronExpression: "0 8 * * *", command: "python wecker.py", isEnabled: false) {{ + setAlarm(id: "{alarm_id}", cronExpression: "0 8 * * *", isEnabled: false) {{ id cronExpression isEnabled