main
tag: launch-2026-06-01
tag: magicmirror-v0.1.1
tag: magicmirror-v0.3.0
tag: mnw-cli-v0.1.2
tag: mnw-cli-v0.1.3
tag: mnw-cli-v0.1.4
tag: pom-v0.4.1
tag: pom-v0.4.2
tag: pom-v0.4.3
tag: pom-v0.4.4
tag: pom-v0.4.5
tag: wam-v0.3.0
tag: wam-v0.3.1
Files
Commits
Tags
Notes
Issues
Enforce >=32-char floor on service secrets; sanitize spool suffix
INTERNAL_SHARED_SECRET and CLI_SERVICE_TOKEN are bearer-token-equivalent but
loaded with no length check, unlike SIGNING_SECRET / SYNCKIT_JWT_SECRET which
already fail closed below 32 chars. Mirror that floor (new WeakInternalSecret /
WeakCliServiceToken config errors) so a weak, brute-forceable secret can't boot.
The scan spool tempfile suffix was a raw s3_key with only '/' replaced. The UUID
already guarantees uniqueness, so the suffix is cosmetic; sanitize it to
[A-Za-z0-9._-] and cap length so a non-server-generated key can never carry path
separators, control chars, or exceed NAME_MAX into the tempfile name.
Co-Authored-By
Claude Opus 4.8 <noreply@anthropic.com>
2 files changed,
+36 insertions,
-5 deletions
342
342
// Postmark inbound email webhook token - optional, inbound endpoint returns 401 if unset
343
343
let postmark_inbound_webhook_token = std::env::var("POSTMARK_INBOUND_WEBHOOK_TOKEN").ok();
344
344
345
-
// Internal shared secret for MT communication
346
-
let internal_shared_secret = std::env::var("INTERNAL_SHARED_SECRET").ok();
345
+
// Internal shared secret for MT communication. Bearer-token-equivalent, so
346
+
// enforce the same >=32-char floor as the signing secrets — a short value
347
+
// is offline-brute-forceable. Fail closed rather than boot on a weak secret.
348
+
let internal_shared_secret = match std::env::var("INTERNAL_SHARED_SECRET") {
349
+
Ok(secret) => {
350
+
if secret.len() < 32 {
351
+
return Err(ConfigError::WeakInternalSecret);
352
+
}
353
+
Some(secret)
354
+
}
355
+
Err(_) => None,
356
+
};
347
357
348
-
// CLI service token for SSH server → internal API authentication
349
-
let cli_service_token = std::env::var("CLI_SERVICE_TOKEN").ok();
358
+
// CLI service token for SSH server → internal API authentication. Same floor.
359
+
let cli_service_token = match std::env::var("CLI_SERVICE_TOKEN") {
360
+
Ok(secret) => {
361
+
if secret.len() < 32 {
362
+
return Err(ConfigError::WeakCliServiceToken);
363
+
}
364
+
Some(secret)
365
+
}
366
+
Err(_) => None,
367
+
};
350
368
351
369
// WAM ticket manager URL (tailnet, e.g. "http://100.x.x.x:7890")
352
370
let wam_url = std::env::var("WAM_URL").ok();
650
668
WeakSigningSecret,
651
669
#[error("SYNCKIT_JWT_SECRET must be at least 32 characters long")]
652
670
WeakSynckitJwtSecret,
671
+
#[error("INTERNAL_SHARED_SECRET must be at least 32 characters long")]
672
+
WeakInternalSecret,
673
+
#[error("CLI_SERVICE_TOKEN must be at least 32 characters long")]
674
+
WeakCliServiceToken,
653
675
}
654
676
655
677
#[cfg(test)]
123
123
// fail `AlreadyExists` — failing a legitimately-running job — and let one
124
124
// scan's `SpoolHandle::drop` unlink the other's live file. The s3_key suffix
125
125
// is retained only for human-readable debugging.
126
-
let suffix = s3_key.replace('/', "_");
126
+
//
127
+
// Sanitize it to a bounded, safe charset: the UUID already guarantees
128
+
// uniqueness, so the suffix is cosmetic, but a raw s3_key could carry path
129
+
// separators, control chars, or exceed NAME_MAX. Map anything outside
130
+
// [A-Za-z0-9._-] to `_` (preserving the old `/`→`_` readability) and cap length.
131
+
let suffix: String = s3_key
132
+
.chars()
133
+
.map(|c| if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') { c } else { '_' })
134
+
.take(64)
135
+
.collect();
127
136
let path = spool_dir.join(format!("scan-{}-{}-{}.tmp", std::process::id(), unique, suffix));
128
137
129
138
// 0o600: the spooled file holds the (possibly paid/private) upload bytes for