{"record":{"id":"d7f8862257a47b5d","repo":"santifer/career-ops","slug":"careerviet-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"careerviet: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}","messagePattern":"careerviet: untrusted hostname \"(.+?)\" — must be (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/careerviet.mjs","lineNumber":110,"sourceCode":"const UPDATED_DATE_RE = /Cập nhật(?:<!--[\\s\\S]*?-->)?\\s*:?\\s*(?:<\\/span>)?\\s*<time>([\\d/-]+)<\\/time>/i;\n\n/** @param {any} ctx @param {number} ms */\nfunction sleep(ctx, ms) {\n  if (typeof ctx?.sleep === 'function') return ctx.sleep(ms);\n  return new Promise((r) => setTimeout(r, ms));\n}\n\n/** @param {string} url */\nfunction assertCareerVietUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`careerviet: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`careerviet: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_HOST) {\n    throw new Error(`careerviet: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}`);\n  }\n  return url;\n}\n\n/**\n * Collapse a markup fragment to its visible text.\n * @param {string} fragment\n * @returns {string}\n */\nexport function visibleText(fragment) {\n  return decodeEntities(\n    String(fragment ?? '')\n      .replace(/<!--[\\s\\S]*?-->/g, ' ')\n      .replace(/<[^>]+>/g, ' '),\n  )\n    .replace(/\\s+/g, ' ')\n    .trim();\n}","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/santifer/career-ops/blob/1696bec4d021768e7359f9aad6b329cba883da20/providers/careerviet.mjs#L92-L128","documentation":"CareerViet allows exactly one hostname: the TRUSTED_HOST constant 'careerviet.vn'. When the parsed URL is https but its hostname differs in any way, this error throws. Unlike the tenant-subdomain providers, there is no regex allowance for subdomains — www.careerviet.vn, any mirror, or a lookalike domain is rejected. This is the provider's SSRF/anti-mirror guard: fetches can only ever land on the genuine board.","triggerScenarios":"Entry configured with a vanity domain, a country mirror, www. prefix, or an entirely different host routed to the careerviet provider; calling assertCareerVietUrl on a redirect target or a crafted URL like https://careerviet.vn.evil.com; hostname with trailing dot (careerviet.vn.) which fails strict equality.","commonSituations":"Pointing the provider at a third-party aggregator URL that happens to list CareerViet jobs; testing against a localhost/staging mirror; accidentally including a path-host mix-up; a scraper that followed a redirect off-domain and validates the result.","solutions":["Set the URL host to exactly careerviet.vn (no www, no subdomain, no trailing dot), e.g. https://careerviet.vn/viec-lam","Do not route third-party/mirror URLs through this provider — it fetches only the official board; use a generic provider for aggregates","If validating redirect outcomes yourself, compare parsed.hostname === 'careerviet.vn' before trusting the response","For local testing, mock at the HTTP layer (fetchJson/fetchText) rather than altering the hostname"],"exampleFix":"// before\ncareers_url: https://www.careerviet.vn/viec-lam\n// after\ncareers_url: https://careerviet.vn/viec-lam","handlingStrategy":"validation","validationCode":"function isTrustedCareervietUrl(u) {\n  try {\n    const p = new URL(u);\n    return p.protocol === 'https:' && p.hostname === 'careerviet.vn';\n  } catch { return false; }\n}\n// before fetch: if (!isTrustedCareervietUrl(entry.careers_url)) skipEntry(entry);","typeGuard":"function asCareervietUrl(value) {\n  if (typeof value !== 'string') return null;\n  try {\n    const p = new URL(value);\n    if (p.protocol !== 'https:' || p.hostname !== 'careerviet.vn') return null;\n    return p;\n  } catch { return null; }\n}","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).includes('untrusted hostname')) {\n    console.error(`${entry.name}: only the exact host careerviet.vn is allowed — no www, subdomains, or mirrors`, err.message);\n  } else throw err;\n}","preventionTips":["Use exactly careerviet.vn as the hostname — strict equality, so no www. and no trailing dot","Never route aggregator or mirror URLs through this provider; it fetches only the official board","If validating redirect outcomes, compare parsed.hostname === 'careerviet.vn' before trusting responses","Mock at the network layer for tests instead of pointing the provider at a different host"],"tags":["ssrf","url-validation","security","provider"],"backgroundTag":"untrusted-host-rejected","analyzedSha":"1696bec4d021768e7359f9aad6b329cba883da20","analyzedAt":"2026-09-01T19:19:23.111Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}