tinyhumansai/openhuman · error

invalid draft id: {id:?}

Error message

invalid draft id: {id:?}

What it means

draft_path validates the id before joining it into a path under the drafts directory: non-empty, at most 64 chars, and only ASCII alphanumeric, '-' and '_'. Anything else — dots, slashes, spaces, unicode — bails. Server-minted draft ids are UUIDs and always pass; the guard exists because an unvalidated join would allow path traversal ('../') out of the drafts dir.

Source

Thrown at src/openhuman/flows/draft_store.rs:42

fn drafts_dir(config: &Config) -> PathBuf {
    config.workspace_dir.join("flows").join("drafts")
}

/// Whether `id` is a safe draft-file stem — guards `get`/`update`/`delete`
/// against path traversal (`..`, separators) since the id reaches the
/// filesystem. Server-minted ids are UUIDs; this only accepts that shape.
fn is_safe_draft_id(id: &str) -> bool {
    !id.is_empty()
        && id.len() <= 64
        && id
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}

/// The on-disk path for draft `id` (validated).
fn draft_path(config: &Config, id: &str) -> Result<PathBuf> {
    if !is_safe_draft_id(id) {
        bail!("invalid draft id: {id:?}");
    }
    Ok(drafts_dir(config).join(format!("{id}.json")))
}

/// Creates a new draft, writes it to disk, and returns it.
pub fn create_draft(
    config: &Config,
    flow_id: Option<String>,
    name: String,
    graph: Value,
    origin: DraftOrigin,
) -> Result<FlowDraft> {
    let now = Utc::now().to_rfc3339();
    let draft = FlowDraft {
        id: Uuid::new_v4().to_string(),
        flow_id,
        name,
        graph,

View on GitHub (pinned to 7491200858)

Solutions

  1. Only use ids returned by create_draft; do not mint ids client-side
  2. If you must pre-validate, mirror the rule: /^[A-Za-z0-9_-]{1,64}$/
  3. Map user-typed names to server ids via the drafts list — never pass names as ids

Example fix

// before
const draft = await loadDraft(userTypedName); // '../../x' → bail

// after
const { id } = await createDraft(...);
const draft = await loadDraft(id); // server-minted UUID
Defensive patterns

Strategy: type-guard

Validate before calling

fn is_safe_draft_id(id: &str) -> bool {
    !id.is_empty()
        && id.len() <= 64
        && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}
// reject before the call: if (!is_safe_draft_id(id)) { return Err(invalid_id); }

Type guard

const SAFE_DRAFT_ID = /^[A-Za-z0-9_-]{1,64}$/;
function isSafeDraftId(id: string): boolean {
  return SAFE_DRAFT_ID.test(id);
}

Prevention

When it happens

Trigger: A client (or hostile/fuzzed caller) supplies a draft id like '../../config', 'my draft', 'a.b', or over 64 chars to any draft read/write/update/delete API that resolves a path; URL-decoded values like '..%2Fx'; ids minted client-side instead of using the one create_draft returned.

Common situations: Frontend generating its own ids; user-typed names fed where ids belong; stored ids surviving a format change; ids containing characters legal elsewhere (dots in filenames) but not here.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/55a7ad72cba66de1. Report an issue: GitHub.