{"record":{"id":"3492a08625e66e15","repo":"gastownhall/beads","slug":"unsafe-dolt-status-table-name-q","errorCode":null,"errorMessage":"unsafe dolt status table name %q","messagePattern":"unsafe dolt status table name %q","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/schema/schema.go","lineNumber":862,"sourceCode":"\tfor table := range tables {\n\t\tnames = append(names, table)\n\t}\n\tsort.Strings(names)\n\treturn names\n}\n\nfunc sortedSignatureTableNames(signatures map[string]string) []string {\n\tnames := make([]string, 0, len(signatures))\n\tfor table := range signatures {\n\t\tnames = append(names, table)\n\t}\n\tsort.Strings(names)\n\treturn names\n}\n\nfunc dirtyTableSignature(ctx context.Context, db DBConn, table string) (string, error) {\n\tif !doltStatusTableNameRE.MatchString(table) {\n\t\treturn \"\", fmt.Errorf(\"unsafe dolt status table name %q\", table)\n\t}\n\t//nolint:gosec // table comes from dolt_status; dolt_diff requires a literal table argument.\n\trows, err := db.QueryContext(ctx, \"SELECT * FROM dolt_diff('HEAD', 'WORKING', \"+sqlStringLiteral(table)+\")\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer rows.Close()\n\n\tcolumns, err := rows.Columns()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar rowSignatures []string\n\tfor rows.Next() {\n\t\tvalues := make([]any, len(columns))\n\t\tdest := make([]any, len(columns))\n\t\tfor i := range values {","sourceCodeStart":844,"sourceCodeEnd":880,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/schema.go#L844-L880","documentation":"dirtyTableSignature interpolates a table name from dolt_status into a dolt_diff('HEAD','WORKING', '<table>') literal, so it first validates the name against doltStatusTableNameRE to prevent SQL injection through a crafted table name. A name that does not match the safe pattern yields \"unsafe dolt status table name %q\" and the diff is never executed.","triggerScenarios":"Any code path fingerprinting dirty tables (dirtyTableSignatures / changedDirtyTableSignatures during MigrateUp) encounters a table name in dolt_status that fails the safe-name regex — e.g. names with quotes, semicolons, backslashes, or other characters outside the allowlist pattern.","commonSituations":"A table created by hand or by another tool with exotic characters in its name inside the .beads Dolt database; a corrupted or tampered dolt_status; a Dolt version producing status entries with quoted/backticked name formatting that beads' regex does not accept.","solutions":["Find the offending table with `dolt status` in the .beads database directory — the message quotes the exact rejected name.","Rename the table to a plain identifier (letters, digits, underscores): `dolt sql -q \"RENAME TABLE \\`bad name\\` TO good_name\"`, then retry `bd`.","If the table is junk created by tooling, drop it: `dolt sql -q \"DROP TABLE \\`bad name\\`\"`.","If dolt_status itself contains entries for nonexistent tables, verify against `dolt status` output and repair the working set (dolt reset --hard only if data loss is acceptable).","If a legitimately simple table name is rejected, check your beads/Dolt version — a regex/version mismatch should be reported upstream."],"exampleFix":"// before: a table named `we;ird'name` in dolt_status poisons the signature pass\nerr := schema.MigrateUp(ctx, db) // unsafe dolt status table name \"we;ird'name\"\n// after: rename to a safe identifier via dolt, then migrate\n//   dolt sql -q \"RENAME TABLE `we;ird'name` TO weird_name\"\nerr = schema.MigrateUp(ctx, db)","handlingStrategy":"validation","validationCode":"// Go: mirror the library's allowlist before creating tables in the beads DB\nvar safeTableRE = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)\nfunc safeTableName(name string) bool { return safeTableRE.MatchString(name) }","typeGuard":"// Go: extract the rejected name from the error message\nfunc unsafeTableName(err error) (string, bool) {\n    const prefix = \"unsafe dolt status table name \"\n    if err == nil || !strings.Contains(err.Error(), prefix) {\n        return \"\", false\n    }\n    msg := err.Error()\n    i := strings.Index(msg, prefix)\n    rest := msg[i+len(prefix):]\n    if len(rest) >= 2 {\n        return strings.Trim(rest, \"\\\"\"), true\n    }\n    return \"\", false\n}","tryCatchPattern":"err := schema.MigrateUp(ctx, db)\nif err != nil {\n    if name, ok := unsafeTableName(err); ok {\n        // rename/drop `name` via dolt, then retry\n        return fmt.Errorf(\"fix table %q (rename to [A-Za-z0-9_]) and retry\", name)\n    }\n    return err\n}","preventionTips":["Only create tables in the beads database with plain identifier names (letters, digits, underscores)","Never hand-edit dolt_status or the Dolt working set","Run `dolt status` after using external tools against the DB to catch oddly named tables early","Keep beads and the Dolt engine version-aligned so status-name formatting matches the allowlist regex"],"tags":["database","dolt","sql-injection","validation"],"backgroundTag":"unsafe-table-name","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}