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
- Only use ids returned by create_draft; do not mint ids client-side
- If you must pre-validate, mirror the rule: /^[A-Za-z0-9_-]{1,64}$/
- 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
- Treat draft ids as opaque server-minted tokens; never construct them from user input
- Validate ids at the API boundary with the same character allowlist
- Reject overlong or oddly-shaped ids early with a 400 rather than letting them reach the store
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
- mascot manifest: missing mascots array
- mascot manifest: no renderable mascots
- audio blob is empty
- audio blob is empty
- Recovery phrase is required.
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/55a7ad72cba66de1.
Report an issue: GitHub.