{"record":{"id":"a1e552d9b8b3d801","repo":"santifer/career-ops","slug":"local-parser-careers-url-must-be-http-s-value","errorCode":null,"errorMessage":"local-parser: careers_url must be http(s): ${value}","messagePattern":"local-parser: careers_url must be http\\(s\\): (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/local-parser.mjs","lineNumber":32,"sourceCode":"\n// `parser.command` / `parser.script` come from portals.yml, which on a shared or\n// template config is not fully trusted. The command must be a known interpreter\n// or a file inside this project — never an arbitrary binary like `rm` or `curl`.\nconst PROJECT_ROOT = realpathSync(resolve(fileURLToPath(new URL('..', import.meta.url))));\nconst ALLOWED_INTERPRETERS = new Set(['python3', 'python', 'node', 'deno', 'bun', 'sh', 'bash']);\n\n// `{careers_url}` and `{company}` are interpolated into the parser's argv. Validate\n// them so an interpolated value can never be read as a CLI flag (argument injection).\nfunction safeCareersUrl(value) {\n  if (!value) return '';\n  let url;\n  try {\n    url = new URL(String(value));\n  } catch {\n    throw new Error(`local-parser: careers_url is not a valid URL: ${value}`);\n  }\n  if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n    throw new Error(`local-parser: careers_url must be http(s): ${value}`);\n  }\n  return url.href;\n}\n\nfunction safeCompany(value) {\n  if (!value) return '';\n  const name = String(value).trim();\n  // execFile passes args verbatim (no shell), so the only injection risk is a\n  // value that begins like a CLI flag.\n  if (name.startsWith('-')) {\n    throw new Error(`local-parser: company name cannot start with '-': ${value}`);\n  }\n  return name;\n}\n\n// Only validate a placeholder's value when the arg actually uses it — a fixed\n// `parser.script` must not be rejected because some unrelated `{company}` value\n// has punctuation it never sees.","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/local-parser.mjs#L14-L50","documentation":"safeCareersUrl() accepts only http: and https: protocols when interpolating {careers_url} into a local parser's argv. A parseable URL with any other scheme (ftp:, file:, javascript:, data:, etc.) is rejected to keep subprocess arguments safe.","triggerScenarios":"An entry's careers_url parses via `new URL()` but its protocol is not http/https — e.g. careers_url: file:///etc/passwd or ftp://acme.com/jobs — and a parser arg references `{careers_url}`.","commonSituations":"Local file paths pasted into careers_url (file:// or bare paths are caught here after URL-parsing quirks); internal ftp links in older configs; someone experimenting with javascript:/data: URLs.","solutions":["Change careers_url in portals.yml to start with https:// (preferred) or http://.","Remove the `{careers_url}` placeholder from parser.args if the parser does not actually need the URL.","Confirm the field is a public web URL, not a local file path — use parser.script pointing at an in-repo file instead.","Wrap fetch() calls in try-catch and surface a clear config-validation message before scan runs."],"exampleFix":"// before (portals.yml)\ncareers_url: file:///home/user/jobs.html\n// after\ncareers_url: https://acme.com/jobs.html","handlingStrategy":"validation","validationCode":"const u = new URL(entry.careers_url);\nif (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error(`${entry.name}: careers_url must be http(s), got ${u.protocol}`);","typeGuard":null,"tryCatchPattern":"try {\n  await localParser.fetch(entry);\n} catch (e) {\n  if (String(e.message).includes('careers_url must be http(s)')) {\n    console.error(`${entry.name}: use a public web URL (https://), not a file/ftp path`);\n    return [];\n  }\n  throw e;\n}","preventionTips":["Treat careers_url as a public web address only — never file:// or internal schemes.","Prefer https:// in all portal entries.","Lint portals.yml for protocol allow-listing before committing config changes.","Keep local file references in parser.script, not careers_url."],"tags":["validation","url","security"],"backgroundTag":"invalid-url","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}