{"record":{"id":"017da59fd7f966a6","repo":"janhq/jan","slug":"invalid-modelid-modelid-only-alphanumeric-and","errorCode":null,"errorMessage":"Invalid modelId: ${modelId}. Only alphanumeric and / _ - . characters are allowed.","messagePattern":"Invalid modelId: (.+?)\\. Only alphanumeric and / _ - \\. characters are allowed\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"extensions/llamacpp-extension/src/index.ts","lineNumber":3086,"sourceCode":"    const task = this.runImport(modelId, opts).finally(() => {\n      this.pendingImports.delete(modelId)\n    })\n    this.pendingImports.set(modelId, task)\n    return task\n  }\n\n  private async runImport(modelId: string, opts: ImportOptions): Promise<void> {\n    const isValidModelId = (id: string) => {\n      // only allow alphanumeric, underscore, hyphen, and dot characters in modelId\n      if (!/^[a-zA-Z0-9/_\\-\\.]+$/.test(id)) return false\n\n      // check for empty parts or path traversal\n      const parts = id.split('/')\n      return parts.every((s) => s !== '' && s !== '.' && s !== '..')\n    }\n\n    if (!isValidModelId(modelId))\n      throw new Error(\n        `Invalid modelId: ${modelId}. Only alphanumeric and / _ - . characters are allowed.`\n      )\n\n    const configPath = await joinPath([\n      await this.getProviderPath(),\n      'models',\n      modelId,\n      'model.yml',\n    ])\n    if (await fs.existsSync(configPath))\n      throw new Error(`Model ${modelId} already exists`)\n\n    // this is relative to Jan's data folder\n    const modelDir = `${this.providerId}/models/${modelId}`\n\n    // we only use these from opts\n    // opts.modelPath: URL to the model file\n    // opts.mmprojPath: URL to the mmproj file","sourceCodeStart":3068,"sourceCodeEnd":3104,"githubUrl":"https://github.com/janhq/jan/blob/fad3f12a147d138388a66f0d92a02b2675f65294/extensions/llamacpp-extension/src/index.ts#L3068-L3104","documentation":"Thrown by runImport() (the private core of import()) when modelId fails isValidModelId. The validator enforces two rules: the regex ^[a-zA-Z0-9/_\\-\\.]+$ (only alphanumerics, slash, underscore, hyphen, dot), and after splitting on '/', no segment may be empty, '.', or '..' (path-traversal and empty-part guard). This blocks both invalid characters and directory-escape attempts before the id is joined into a filesystem path.","triggerScenarios":"Passing a modelId with spaces, colons, or unicode (e.g. 'my model: v2'); passing an absolute or relative path ('../evil', '/etc/x'); passing '//double' or trailing '/' producing empty segments ('foo/'); passing 'a/../b' or './x'; passing a URL as the modelId.","commonSituations":"User pastes a HuggingFace repo id with a colon tag (org/model:main). UI fails to slugify the id. Auto-generated id from a filename containing spaces or parentheses. Security-sensitive: malicious or accidental path traversal that would write model.yml outside the models directory.","solutions":["Sanitize modelId to only [a-zA-Z0-9/_-.] before calling import - replace spaces with hyphens, strip other punctuation.","Remove any ':tag' suffix (e.g. ':main', ':v1') from the id; tags belong in opts, not the id.","Collapse multiple slashes and trim leading/trailing slashes so no empty segments remain.","Reject ids containing '..' or '.' segments at the source (UI validation) before reaching the extension."],"exampleFix":"// before\nawait provider.import('org/model:main', opts) // throws - colon\n// after\nconst slug = 'org/model-main'.replace(/[^a-zA-Z0-9/_\\-.]/g, '-').replace(/\\/+/g, '/')\nif (/^[a-zA-Z0-9/_\\-.]+$/.test(slug) && slug.split('/').every(s => s && s !== '.' && s !== '..')) {\n  await provider.import(slug, opts)\n}","handlingStrategy":"validation","validationCode":"// Pre-validate modelId with the same rules the extension enforces\nfunction isValidModelId(id: string): boolean {\n  if (!/^[a-zA-Z0-9/_\\-.]+$/.test(id)) return false\n  return id.split('/').every(s => s !== '' && s !== '.' && s !== '..')\n}\nif (!isValidModelId(modelId)) throw new Error(`modelId '${modelId}' rejected by client-side validation`)","typeGuard":"function isValidModelId(id: string): boolean {\n  if (!/^[a-zA-Z0-9/_\\-.]+$/.test(id)) return false\n  return id.split('/').every(s => s !== '' && s !== '.' && s !== '..')\n}","tryCatchPattern":"try { await provider.import(modelId, opts) }\ncatch (e) {\n  if (/Invalid modelId/.test(String(e))) { modelId = slugify(modelId); await provider.import(modelId, opts) }\n  else throw e\n}","preventionTips":["Slugify user-entered ids at the UI layer (strip colons/tags, replace spaces).","Block ':tag' suffixes from ever reaching import.","Run the same regex the extension uses as a form validator."],"tags":["model","import","validation","path-traversal","security","sanitization"],"backgroundTag":null,"analyzedSha":"fad3f12a147d138388a66f0d92a02b2675f65294","analyzedAt":"2026-08-12T20:33:47.516Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}