JWT tokens without both 'iss' and 'aud' are no longer reported as active credentials

This commit is contained in:
Mick Grove 2025-08-07 18:30:40 -07:00
commit 0bdd68c900
5 changed files with 32 additions and 16 deletions

View file

@ -75,8 +75,6 @@ impl GitHubRepoSpecifiers {
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
#[strum(serialize_all = "kebab-case")]
pub enum GitHubRepoType {
/// Both source and fork repositories
All,
/// Only source repositories (not forks)
Source,
/// Only fork repositories
@ -87,7 +85,6 @@ pub enum GitHubRepoType {
impl From<GitHubRepoType> for crate::github::RepoType {
fn from(val: GitHubRepoType) -> Self {
match val {
GitHubRepoType::Source => crate::github::RepoType::All,
GitHubRepoType::Source => crate::github::RepoType::Source,
GitHubRepoType::Fork => crate::github::RepoType::Fork,
}

View file

@ -104,7 +104,7 @@ pub async fn enumerate_repo_urls(
}
}
let projects_ep = builder.build()?; // now no borrows of a temporary
let projects_ep = builder.build()?; // now no borrows of a temporary
let projects: Vec<SimpleProject> = projects_ep.query(&client)?;
for proj in projects {

View file

@ -69,6 +69,26 @@ pub async fn validate_jwt(token: &str) -> Result<(bool, String)> {
}
}
let header_b64 = token.split('.').next().ok_or_else(|| anyhow!("invalid JWT format"))?;
let header_json =
URL_SAFE_NO_PAD.decode(header_b64).map_err(|e| anyhow!("invalid base64 in header: {e}"))?;
let header_val: serde_json::Value =
serde_json::from_slice(&header_json).map_err(|e| anyhow!("invalid header json: {e}"))?;
let alg_str = header_val.get("alg").and_then(|v| v.as_str()).unwrap_or("");
// If alg is "none", skip signature/JWKS entirely
if alg_str.eq_ignore_ascii_case("none") {
// still enforce your time/claims checks that already ran
return Ok((
true,
format!(
"JWT valid (alg: none, iss: {}, aud: {:?})",
claims.iss.clone().unwrap_or_default(),
extract_aud_strings(&claims),
),
));
}
// ---------------------------------------------------------------------------
let issuer = claims.iss.clone().unwrap_or_default();
let aud_strings = extract_aud_strings(&claims);
@ -200,7 +220,13 @@ mod tests {
fn build_token(exp_offset: i64) -> String {
let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"none"}"#);
let exp = (Utc::now() + ChronoDuration::seconds(exp_offset)).timestamp();
let payload = URL_SAFE_NO_PAD.encode(format!("{{\"exp\":{exp}}}"));
let payload = URL_SAFE_NO_PAD.encode(format!(
r#"{{
"exp": {exp},
"iss": "https://example.com",
"aud": ["test-audience"]
}}"#
));
format!("{header}.{payload}.")
}

View file

@ -112,4 +112,4 @@ async fn test_redact_hashes_finding_values() -> Result<()> {
}
Ok(())
}
}

View file

@ -5,12 +5,7 @@ use serde_json::Value;
#[test]
fn scan_rules_has_no_validated_findings() -> Result<()> {
let output = Command::cargo_bin("kingfisher")?
.args([
"scan", "data/rules",
"--format", "json",
"--no-update-check",
"--only-valid",
])
.args(["scan", "data/rules", "--format", "json", "--no-update-check", "--only-valid"])
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
@ -52,10 +47,8 @@ fn scan_rules_has_no_validated_findings() -> Result<()> {
let rule_id = finding["rule"]["id"].as_str().unwrap_or("unknown");
let rule_prevalidated = finding["rule"]["prevalidated"].as_bool().unwrap_or(false);
let status = finding["finding"]["validation"]["status"]
.as_str()
.unwrap_or("")
.to_ascii_lowercase();
let status =
finding["finding"]["validation"]["status"].as_str().unwrap_or("").to_ascii_lowercase();
let response = finding["finding"]["validation"]["response"]
.as_str()