Better app name validation for custom.txt generator
build / build-linux-amd64 (push) Successful in 1m56s
build / build-linux-amd64 (push) Successful in 1m56s
This commit is contained in:
@@ -95,6 +95,9 @@ pub async fn generate(
|
||||
Some(("error", "App name is required.")),
|
||||
)));
|
||||
}
|
||||
if let Err(msg) = validate_app_name(state.app_name.trim()) {
|
||||
return Ok(Html(render_form(&state, &pubkey, "", Some(("error", msg)))));
|
||||
}
|
||||
if logo_too_big {
|
||||
return Ok(Html(render_form(
|
||||
&state,
|
||||
@@ -158,15 +161,21 @@ pub async fn generate(
|
||||
if !default_obj.is_empty() {
|
||||
payload.insert("default-settings".into(), Value::Object(default_obj));
|
||||
}
|
||||
if !override_obj.is_empty() {
|
||||
payload.insert("override-settings".into(), Value::Object(override_obj));
|
||||
// `app-icon` must live inside `override-settings`, not at top level. The
|
||||
// client routes top-level string keys into HARD_SETTINGS, but the Flutter
|
||||
// logo loader (`mainGetBuildinOption('app-icon')`) reads from
|
||||
// BUILTIN_SETTINGS — and BUILTIN_SETTINGS is only populated when a key is
|
||||
// matched against KEYS_BUILDIN_SETTINGS inside default-/override-settings.
|
||||
let mut overrides = override_obj;
|
||||
if let Some(bytes) = &logo_bytes {
|
||||
overrides.insert("app-icon".into(), json!(base64::encode(bytes)));
|
||||
}
|
||||
if !overrides.is_empty() {
|
||||
payload.insert("override-settings".into(), Value::Object(overrides));
|
||||
}
|
||||
for (k, v) in collect_kv_pairs(&state.kv_keys, &state.kv_values) {
|
||||
payload.insert(k, json!(v));
|
||||
}
|
||||
if let Some(bytes) = &logo_bytes {
|
||||
payload.insert("app-icon".into(), json!(base64::encode(bytes)));
|
||||
}
|
||||
|
||||
let body = match serde_json::to_vec(&Value::Object(payload)) {
|
||||
Ok(v) => v,
|
||||
@@ -220,6 +229,50 @@ struct FormState {
|
||||
kv_values: Vec<String>,
|
||||
}
|
||||
|
||||
/// Reject app names that won't survive the round-trip through the patched
|
||||
/// client's Windows install/registry/service code paths. Spaces are fine
|
||||
/// (e.g. "Nintendo Support"), but path separators, quotes, shell
|
||||
/// metacharacters, and control characters are not — `get_app_name()` is
|
||||
/// interpolated unquoted into batch scripts, registry subkeys, install
|
||||
/// dirs, service names, and named-pipe paths. We also reject Windows
|
||||
/// reserved device names (CON/PRN/AUX/NUL/COM1-9/LPT1-9). This does not
|
||||
/// change anything about the signing pipeline; it just stops operators
|
||||
/// from baking foot-guns into custom.txt.
|
||||
fn validate_app_name(name: &str) -> Result<(), &'static str> {
|
||||
if name.len() > 64 {
|
||||
return Err("App name must be 64 characters or fewer.");
|
||||
}
|
||||
// Disallowed: ASCII control chars, Windows path-illegal chars
|
||||
// (`<>:"/\|?*`), and shell-injection-prone chars (`& ; $ \` \n` etc.).
|
||||
// Allowed printable punctuation is limited to space, '-', '_', '.', '(',
|
||||
// ')' which is enough for human-friendly names like "Acme Remote (EU)".
|
||||
const ALLOWED_PUNCT: &[char] = &[' ', '-', '_', '.', '(', ')'];
|
||||
if !name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || ALLOWED_PUNCT.contains(&c))
|
||||
{
|
||||
return Err("App name may only contain ASCII letters, digits, spaces, and `- _ . ( )`. Other punctuation breaks Windows install paths.");
|
||||
}
|
||||
if name.starts_with(' ') || name.ends_with(' ') {
|
||||
return Err("App name must not start or end with a space.");
|
||||
}
|
||||
if name.contains(" ") {
|
||||
return Err("App name must not contain consecutive spaces.");
|
||||
}
|
||||
let upper = name.to_ascii_uppercase();
|
||||
const RESERVED: &[&str] = &["CON", "PRN", "AUX", "NUL"];
|
||||
if RESERVED.contains(&upper.as_str()) {
|
||||
return Err("App name conflicts with a Windows reserved device name.");
|
||||
}
|
||||
if (upper.starts_with("COM") || upper.starts_with("LPT"))
|
||||
&& upper.len() == 4
|
||||
&& upper.as_bytes()[3].is_ascii_digit()
|
||||
{
|
||||
return Err("App name conflicts with a Windows reserved device name.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_settings_json(raw: &str) -> Result<Map<String, Value>, String> {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
|
||||
Reference in New Issue
Block a user