Skip to content

Potential fix for code scanning alert no. 1: Uncontrolled command line#10

Open
SimoleS wants to merge 1 commit into
mainfrom
fix-uncontrolled-cmd
Open

Potential fix for code scanning alert no. 1: Uncontrolled command line#10
SimoleS wants to merge 1 commit into
mainfrom
fix-uncontrolled-cmd

Conversation

@SimoleS

@SimoleS SimoleS commented May 26, 2026

Copy link
Copy Markdown
Contributor

Potential fix for https://github.com/PlayUPSK/Keyward/security/code-scanning/1

The best fix is to validate and constrain all untrusted values that become ssh-keygen arguments, especially principals, key_id, and ttl_seconds, before constructing cmd. Keep the hardcoded binary and list-style subprocess.run, but reject unsafe characters and empty/invalid values. This preserves behavior for valid inputs while blocking malicious or malformed inputs.

Implement in platform/app/services/ssh_signer.py inside sign_user_certificate:

  • Add strict validation for:
    • ttl_seconds > 0
    • each principal: non-empty and only safe characters (for example [A-Za-z0-9._@:-]+)
    • key_id: non-empty and limited to safe printable chars used by your existing format (allow = ; _ - . @ : and alnum)
  • Raise SSHSigningError on invalid input.
  • Use validated principal list when joining for -n.

No functional change for normal valid requests; only unsafe values are rejected early.

Suggested fixes powered by Copilot Autofix. Review carefully before merging.

Summary by CodeRabbit

  • Bug Fixes
    • Improved SSH certificate signing robustness by enforcing stricter input validation. The system now rejects invalid principal formats, malformed key identifiers, and non-positive validity periods before attempting certificate generation.
    • Enhanced error handling with clearer validation failure messages to help identify configuration issues earlier and prevent downstream processing errors.

Review Change Stack

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR adds regex-based input validation to ssh_signer.py before invoking ssh-keygen. The module now imports re, defines allowlists for principal and key_id formats, validates inputs in sign_user_certificate, and uses the sanitized principals list when constructing the command.

Changes

SSH Signer Input Validation

Layer / File(s) Summary
Validation setup and regex patterns
platform/app/services/ssh_signer.py
Imports re module and defines module-level compiled regex allowlists for safe principal and key_id formats.
Validation enforcement in sign_user_certificate
platform/app/services/ssh_signer.py
Validates ttl_seconds (positive), key_id (matches allowlist), and each principal (matches allowlist); builds validated_principals list and uses it in the ssh-keygen -n argument instead of raw input.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A signer so wise, now guards the front door,
With regex allowlists that filter and score,
Rejecting the chaos, the invalid keys,
SSH certificates signed with such ease! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly reflects the main change: validating inputs to prevent uncontrolled command line injection in the ssh_signer module.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-uncontrolled-cmd

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@SimoleS SimoleS marked this pull request as ready for review May 26, 2026 22:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@platform/app/services/ssh_signer.py`:
- Around line 31-32: Add an upper-bound TTL check alongside the existing
positive check in the function that validates ttl_seconds (the block that raises
SSHSigningError("ttl_seconds must be positive")), e.g., enforce ttl_seconds <=
31536000 (1 year) or a configurable MAX_TTL_SECONDS constant; if the value
exceeds the limit, raise SSHSigningError with a clear message referencing the
limit so callers know why it failed. Ensure to define or reference a
MAX_TTL_SECONDS constant near the validation and update any related input docs
or error handling to reflect the new bound.
- Around line 36-40: The error thrown when validating principals (in the loop
over principals that builds validated_principals and uses _SAFE_PRINCIPAL_RE) is
too generic; update the SSHSigningError to include a clearer message that
identifies the failing principal (if safe) or at least the expected format/regex
so users can debug (e.g., "invalid ssh principal: '<principal>'" or "invalid ssh
principal format — must match <pattern>"); modify the raise inside the loop to
reference the current principal and/or _SAFE_PRINCIPAL_RE so the message is
descriptive while avoiding logging sensitive data.
- Around line 13-14: The regexes _SAFE_PRINCIPAL_RE and _SAFE_KEY_ID_RE
currently restrict characters used for ssh_principal (passed to ssh-keygen -n)
and key_id (passed to ssh-keygen -I) but do not guarantee that request_id in the
incoming payload matches the allowed charset, which can cause "invalid key_id
format" failures; update the code to validate request_id early (before building
key_id) against the same safe character set used by _SAFE_KEY_ID_RE (or
constrain/normalize request_id to that set) and/or update the regexes for
clarity (e.g., move or escape '-' in _SAFE_PRINCIPAL_RE and _SAFE_KEY_ID_RE) and
add a concise comment/docstring explaining the expected format for ssh_principal
and request_id so callers know the allowed characters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 35bc1855-4c81-4b6b-9114-e8b8b4a27643

📥 Commits

Reviewing files that changed from the base of the PR and between 389eb12 and c04359e.

📒 Files selected for processing (1)
  • platform/app/services/ssh_signer.py

Comment on lines +13 to +14
_SAFE_PRINCIPAL_RE = re.compile(r"^[A-Za-z0-9._@:-]+$")
_SAFE_KEY_ID_RE = re.compile(r"^[A-Za-z0-9._@:=;-]+$")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for SSH principal and key_id usage patterns in tests or docs

# Look for test files that might show example principals/key_ids
rg -n -C3 'sign_user_certificate|principals.*=|key_id.*=' --type=py -g '!**/ssh_signer.py'

# Search for any SSH documentation
fd -e md -e txt -e rst | xargs rg -l -i 'ssh.*principal|key.*id' 2>/dev/null || echo "No docs found"

Repository: PlayUPSK/Keyward

Length of output: 7198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect ssh_signer.py around the regex definitions and where they’re used
sed -n '1,120p' platform/app/services/ssh_signer.py

echo "---- usages ----"
rg -n "_SAFE_PRINCIPAL_RE|_SAFE_KEY_ID_RE" platform/app/services/ssh_signer.py

# 2) Look for principal/key_id examples in code/docs beyond the earlier search
echo "---- ssh_principal usage examples ----"
rg -n "ssh_principal|cert_key_id|key_id=" platform -S

echo "---- docs search (broader) ----"
rg -n -i "ssh.*principal|principal.*ssh|cert_key_id|key id|key_id" README.md platform -S || true

Repository: PlayUPSK/Keyward

Length of output: 11460


🌐 Web query:

ssh-keygen -n principals allowed characters comma separated list certificate

💡 Result:

When using the ssh-keygen command to sign a certificate, the -n flag allows you to specify one or more principals (user or host names) to be included in the certificate [1][2]. Multiple principals must be provided as a comma-separated list [1][3]. Regarding formatting and allowed characters: - Comma-Separated List: You should provide the list as a single string of comma-separated values without spaces between them (e.g., -n principal1,principal2,principal3) [1][3]. - Allowed Characters: While there is no rigid, single character-set specification in the OpenSSH manual, principals are treated as strings representing identities (users or hosts). For interoperability and to avoid parsing issues, it is strongly recommended to stick to standard alphanumeric characters, underscores, hyphens, and dots. Avoid using commas (except as the separator), spaces, or shell-metacharacters that might be misinterpreted by the SSH client or server during processing. Principals are ultimately matched against patterns defined on the server side in either an authorized_principals file (specified by AuthorizedPrincipalsFile in sshd_config) or directly within an authorized_keys file [1][3][4]. Because these files support pattern matching (including wildcards like *), using characters that have special meaning in glob patterns or regex could lead to unexpected behavior [1][2][3]. It is best practice to keep principal names simple and predictable [3].

Citations:


🌐 Web query:

ssh-keygen -I key ID allowed characters format certificate

💡 Result:

In OpenSSH, the ssh-keygen “-I key_id” argument is a free-form “key identifier” for certificates. OpenSSH’s certificate protocol specifies that the certificate’s “key id” is a free-form text field filled in by the CA at signing time; it’s used for identification and is logged in server logs. [1][2] Allowed characters / restrictions: the sources above explicitly describe key_id/key id as free-form text and do not define a character whitelist or a specific character-format constraint in the documented interface. [1][2] Therefore, there is no authoritative published “allowed characters” set for “-I key ID”; any practical restrictions would come from parsing/quoting and any downstream log/format usage, not from a documented character whitelist. [1][2] Practical note: treat key_id as an arbitrary string suitable for the shell/command line you provide, but do not assume it will be validated against a strict regex like “alnum + dash + underscore” because OpenSSH documents it as free-form. [1][2] Answering your exact query terms: - “ssh-keygen -I key ID allowed characters”: none are specified by OpenSSH docs/protocol; it is free-form. [1][2] - “format certificate”: -I is used when signing to set the certificate’s key identifier field; the underlying certificate format includes “string key id” as a string field. [2][1]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show README section around the "SSH principal" mention
rg -n "SSH principal" README.md
python3 - <<'PY'
import re, pathlib
p=pathlib.Path("README.md")
lines=p.read_text(encoding="utf-8").splitlines()
for i,l in enumerate(lines,1):
    if "SSH principal" in l:
        start=max(1,i-10); end=min(len(lines), i+40)
        for j in range(start,end+1):
            print(f"{j}:{lines[j-1]}")
        print("----")
PY

# Show admin form input placeholder for ssh_principal (may hint allowed examples)
rg -n "name=\"ssh_principal\"|SSH Principal" platform/app/templates/admin/policies.html
sed -n '560,640p' platform/app/templates/admin/policies.html

Repository: PlayUPSK/Keyward

Length of output: 5951


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '120,220p' platform/app/services/ssh_ca.py
sed -n '1,120p' platform/app/services/policy_engine.py
sed -n '180,260p' platform/app/services/policy_engine.py

Repository: PlayUPSK/Keyward

Length of output: 8962


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect where request_id is validated/constructed in ssh_ca and related flows
rg -n "request_id" platform/app/services/ssh_ca.py platform/app -S
rg -n "payload\\[\"request_id\"\\]|\\brequest_id\\b" platform/app/services/ssh_ca.py -n
sed -n '1,120p' platform/app/services/ssh_ca.py
sed -n '120,260p' platform/app/services/ssh_ca.py

Repository: PlayUPSK/Keyward

Length of output: 12389


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "request_id" platform/tests platform/app -S

Repository: PlayUPSK/Keyward

Length of output: 2178


Document and align the SSH principal/key_id validation with expected inputs

  • _SAFE_PRINCIPAL_RE is applied via fullmatch to payload["ssh_principal"] before passing it to ssh-keygen -n as a single principal (comma-separated list is handled by the code). The whitelist blocks commas/spaces/metacharacters and matches the repo’s documented/UI examples (alice, deploy, root, ubuntu).
  • _SAFE_KEY_ID_RE is applied via fullmatch to key_id passed to ssh-keygen -I. Since key_id is built as request=<request_id>;user=<uuid>;device=<uuid>;server=<uuid>, the pattern’s allowance for =, ;, -, etc. is necessary and currently covers that format. However request_id is otherwise free-form in the request payload, so any unexpected characters will trigger invalid key_id format and fail issuance—validate request_id earlier to the same character set or document the required format.
  • Optional clarity: the trailing - in [A-Za-z0-9._@:-] / [A-Za-z0-9._@:=;-] already behaves as a literal hyphen, but moving - to the start/escaping it would make the intent clearer.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/app/services/ssh_signer.py` around lines 13 - 14, The regexes
_SAFE_PRINCIPAL_RE and _SAFE_KEY_ID_RE currently restrict characters used for
ssh_principal (passed to ssh-keygen -n) and key_id (passed to ssh-keygen -I) but
do not guarantee that request_id in the incoming payload matches the allowed
charset, which can cause "invalid key_id format" failures; update the code to
validate request_id early (before building key_id) against the same safe
character set used by _SAFE_KEY_ID_RE (or constrain/normalize request_id to that
set) and/or update the regexes for clarity (e.g., move or escape '-' in
_SAFE_PRINCIPAL_RE and _SAFE_KEY_ID_RE) and add a concise comment/docstring
explaining the expected format for ssh_principal and request_id so callers know
the allowed characters.

Comment on lines +31 to +32
if ttl_seconds <= 0:
raise SSHSigningError("ttl_seconds must be positive")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

TTL validation looks good; consider adding an upper bound.

The positive check prevents invalid TTL values. You might also consider adding an upper bound to prevent unexpectedly long-lived certificates (e.g., ttl_seconds > 31536000 for certificates longer than 1 year), depending on your security policy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/app/services/ssh_signer.py` around lines 31 - 32, Add an upper-bound
TTL check alongside the existing positive check in the function that validates
ttl_seconds (the block that raises SSHSigningError("ttl_seconds must be
positive")), e.g., enforce ttl_seconds <= 31536000 (1 year) or a configurable
MAX_TTL_SECONDS constant; if the value exceeds the limit, raise SSHSigningError
with a clear message referencing the limit so callers know why it failed. Ensure
to define or reference a MAX_TTL_SECONDS constant near the validation and update
any related input docs or error handling to reflect the new bound.

Comment on lines +36 to +40
validated_principals: list[str] = []
for principal in principals:
if not principal or not _SAFE_PRINCIPAL_RE.fullmatch(principal):
raise SSHSigningError("invalid ssh principal format")
validated_principals.append(principal)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider more descriptive error message.

The error "invalid ssh principal format" doesn't indicate which principal failed or what format is expected. This could make debugging difficult for users.

📝 Suggested improvement for error clarity
 validated_principals: list[str] = []
 for principal in principals:
     if not principal or not _SAFE_PRINCIPAL_RE.fullmatch(principal):
-        raise SSHSigningError("invalid ssh principal format")
+        raise SSHSigningError(
+            f"invalid ssh principal format: {principal!r}. "
+            "Principals must contain only alphanumeric, dot, underscore, @, colon, and hyphen."
+        )
     validated_principals.append(principal)

Note: Only include the actual principal value in the error if it's safe to log (i.e., doesn't contain sensitive data). Alternatively, just improve the general error message without including the value.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
validated_principals: list[str] = []
for principal in principals:
if not principal or not _SAFE_PRINCIPAL_RE.fullmatch(principal):
raise SSHSigningError("invalid ssh principal format")
validated_principals.append(principal)
validated_principals: list[str] = []
for principal in principals:
if not principal or not _SAFE_PRINCIPAL_RE.fullmatch(principal):
raise SSHSigningError(
f"invalid ssh principal format: {principal!r}. "
"Principals must contain only alphanumeric, dot, underscore, @, colon, and hyphen."
)
validated_principals.append(principal)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/app/services/ssh_signer.py` around lines 36 - 40, The error thrown
when validating principals (in the loop over principals that builds
validated_principals and uses _SAFE_PRINCIPAL_RE) is too generic; update the
SSHSigningError to include a clearer message that identifies the failing
principal (if safe) or at least the expected format/regex so users can debug
(e.g., "invalid ssh principal: '<principal>'" or "invalid ssh principal format —
must match <pattern>"); modify the raise inside the loop to reference the
current principal and/or _SAFE_PRINCIPAL_RE so the message is descriptive while
avoiding logging sensitive data.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant