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
|