fix(aws): refuse credentials in repo URLs and HEAD as a branch

- validate_repo_url rejects https URLs with userinfo (user:pass@ or
  token@), pointing at the project's Git HTTPS token setting (m4).
- validate_branch rejects HEAD (m5).
- The FIXED_ENVIRONMENT doc comment sits on FIXED_ENVIRONMENT again (m6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-11-15 12:34:03 -08:00
1 parent 2772647690
commit 7f113f58ac
2 files changed
+33 -2

No files matched your search

+1 -1
View File
@@ -74,10 +74,10 @@ const KEY_REPOS: &str = "TRIPLEC_REPOS";
const KEY_WIP_PUSH: &str = "TRIPLEC_WIP_PUSH";
const KEY_IDLE: &str = "TRIPLEC_IDLE_STOP_MINUTES";
const KEY_STORAGE: &str = "TRIPLEC_STORAGE";
/// The task definition's whole `environment` (R2): constants, no project data.
/// Seconds ECS waits after SIGTERM before SIGKILL (Fargate allows 2–120).
pub const STOP_TIMEOUT_SECS: i32 = 120;
/// The task definition's whole `environment` (R2): constants, no project data.
const FIXED_ENVIRONMENT: [(&str, &str); 2] = [("TRIPLEC_HOST", "aws"), ("COLORTERM", "truecolor")];
fn other(message: impl Into<String>) -> AwsError {
+32 -1
View File
@@ -585,6 +585,15 @@ fn validate_repo_url(url: &str) -> Result<(), String> {
return bad("must not start with '-'");
}
let ok = if let Some(rest) = url.strip_prefix("https://") {
// Credentials in the URL would sit in the project secret and in
// `.git/config` in the clear, and leak into every clone's remote.
let authority = rest.split(['/', '?', '#']).next().unwrap_or("");
if authority.contains('@') {
return bad(
"must not carry credentials (user:password@ or token@); put the token in the \
project's Git HTTPS token setting (Access) instead",
);
}
!rest.is_empty()
} else if let Some(rest) = url.strip_prefix("ssh://") {
!rest.is_empty()
@@ -610,6 +619,11 @@ fn validate_branch(branch: &str) -> Result<(), String> {
if branch.starts_with('-') || branch.starts_with('/') || branch.ends_with('/') {
return bad("must not start with '-' or start/end with '/'");
}
// `HEAD` passes check-ref-format but `git clone --branch HEAD` fails
// (and a branch named HEAD is refused by `git branch`).
if branch == "HEAD" {
return bad("HEAD is not a branch; leave the branch empty for the default branch");
}
if branch == "@" || branch.ends_with('.') || branch.contains("..") || branch.contains("@{") || branch.contains("//") {
return bad("is not a valid branch name");
}
@@ -1405,13 +1419,30 @@ mod tests {
}
}
#[test]
fn an_https_url_with_credentials_is_refused_pointing_at_the_token_setting() {
let check = |u: &str| validate_aws_workspace(&with_repos(vec![repo(u, None, "x")]));
for bad in [
"https://user:pass@github.com/o/r.git",
"https://ghp_abc@github.com/o/r.git",
"https://oauth2:tok@gitlab.com/o/r",
] {
let e = check(bad).unwrap_err();
assert!(e.contains("Git HTTPS token"), "{e}");
}
// An @ in the path is not userinfo; ssh and scp forms keep their user.
for good in ["https://h/o/r@v2.git", "ssh://git@h/o/r.git", "git@github.com:o/r.git"] {
assert!(check(good).is_ok(), "{good}");
}
}
#[test]
fn branches_follow_check_ref_format() {
let ok = |b: Option<&str>| validate_aws_workspace(&with_repos(vec![repo("https://h/r", b, "x")])).is_ok();
for good in [None, Some("main"), Some("feat/aws-1"), Some("v1.2.3"), Some("a@b")] {
assert!(ok(good), "{good:?}");
}
for bad in ["", "-x", "a..b", "a b", "a~1", "a^", "a:b", "a?", "a*", "a[", "a\\b", "a.lock", "x/.hidden", "a/", "/a", "a//b", "a@{1}", "@", "a."] {
for bad in ["", "-x", "a..b", "a b", "a~1", "a^", "a:b", "a?", "a*", "a[", "a\\b", "a.lock", "x/.hidden", "a/", "/a", "a//b", "a@{1}", "@", "a.", "HEAD"] {
assert!(!ok(Some(bad)), "{bad:?}");
}
}