{"record":{"id":"680f9583ad9c09b1","repo":"santifer/career-ops","slug":"oraclecloud-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"oraclecloud: untrusted hostname \"${parsed.hostname}\" — must match *.fa[.<region>][.ocs].oraclecloud[1-99].com","messagePattern":"oraclecloud: untrusted hostname \"(.+?)\" — must match \\*\\.fa\\[\\.<region>\\]\\[\\.ocs\\]\\.oraclecloud\\[1-99\\]\\.com","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/oraclecloud.mjs","lineNumber":70,"sourceCode":"const PAGE_SIZE = 200;\nconst MAX_PAGES = 25;             // safety cap (~5000 jobs); hard ceiling like workday\nconst RETRY_POLICY = { retries: 3 };\nconst INTER_PAGE_DELAY_MS = 250;  // WAF-aware spacing between same-host pages\n\n// facetsList is a fixed constant on the finder; %3B is the encoded ';' separator.\nconst FACETS_LIST = 'LOCATIONS%3BWORK_LOCATIONS%3BWORKPLACE_TYPES%3BTITLES%3BCATEGORIES%3BORGANIZATIONS%3BPOSTING_DATES%3BFLEX_FIELDS';\n\n/** @param {string} url */\nfunction assertOracleUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`oraclecloud: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`oraclecloud: URL must use HTTPS: ${url}`);\n  if (!ORACLE_HOST_RE.test(parsed.hostname)) {\n    throw new Error(`oraclecloud: untrusted hostname \"${parsed.hostname}\" — must match *.fa[.<region>][.ocs].oraclecloud[1-99].com`);\n  }\n  return url;\n}\n\n// NaN-safe Date.parse — `|| undefined` would also coerce a valid epoch 0.\n// (copied from greenhouse.mjs)\nfunction toEpochMs(value) {\n  if (!value) return undefined;\n  const parsed = Date.parse(value);\n  return Number.isNaN(parsed) ? undefined : parsed;\n}\n\nfunction sleep(ms, ctx) {\n  if (typeof ctx?.sleep === 'function') return ctx.sleep(ms);\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/providers/oraclecloud.mjs#L52-L88","documentation":"assertOracleUrl validates a careers URL before any network request is made to Oracle Recruiting (Fusion) boards. The URL parsed fine and used HTTPS, but its hostname does not match the allowlist regex ORACLE_HOSTRE (/^[a-z0-9-]+\\.fa\\.(?:[a-z0-9-]+\\.)?(?:ocs\\.)?oraclecloud(?:[1-9][0-9]?)?\\.com$/i), so the provider refuses to send a request to a host it does not recognize. This is an SSRF/trust guard, not a network failure — the request was never attempted.","triggerScenarios":"Calling providers/oraclecloud.mjs fetch (directly or via scan.mjs/verify-portals.mjs) with an entry whose careers_url points at a hostname outside the Oracle Fusion pattern, e.g. a custom vanity domain, a non-FA Oracle host (myhost.oraclecloud.com without .fa), a region misspelled, or a subdomain typo like acme.fa..oraclecloud.com.","commonSituations":"portals.yml misconfiguration: someone pasted the marketing careers page URL instead of the Fusion board URL; a company migrated to a vanity CNAME (careers.acme.com); the tenant uses a new regional host shape the regex doesn't yet allow; or a trailing/extra dot in the hostname.","solutions":["Open portals.yml and fix the entry's careers_url to the real Fusion board host matching <tenant>.fa[.<region>][.ocs].oraclecloud[1-99].com (e.g. https://acme.fa.ocs.oraclecloud.com).","Verify the hostname with the regex: node -e \"console.log(/^[a-z0-9-]+\\\\.fa\\\\.(?:[a-z0-9-]+\\\\.)?(?:ocs\\\\.)?oraclecloud(?:[1-9][0-9]?)?\\\\.com$/i.test('acme.fa.ocs.oraclecloud.com'))\".","If Oracle genuinely serves this tenant on a new host shape, extend ORACLE_HOST_RE in providers/oraclecloud.mjs (line 50) and update the error message to match.","Point the entry at the tenant's API base URL rather than a vanity domain; the provider derives API calls from the allowlisted host."],"exampleFix":"// before (portals.yml)\ncareers_url: https://careers.acme.com/jobs\n// after\ncareers_url: https://acme.fa.ocs.oraclecloud.com/hcmUI/CandidateExperience/en/sites/CX_1","handlingStrategy":"validation","validationCode":"const ORACLE_HOST_RE = /^[a-z0-9-]+\\.fa\\.(?:[a-z0-9-]+\\.)?(?:ocs\\.)?oraclecloud(?:[1-9][0-9]?)?\\.com$/i;\nexport function isOracleUrl(url) {\n  try {\n    const u = new URL(url);\n    return u.protocol === 'https:' && ORACLE_HOST_RE.test(u.hostname);\n  } catch { return false; }\n}\nif (!isOracleUrl(entry.careers_url)) throw new Error(`oraclecloud: untrusted or invalid careers_url for ${entry.name}`);","typeGuard":"function isOracleUrl(u) {\n  if (typeof u !== 'string') return false;\n  try {\n    const parsed = new URL(u);\n    return parsed.protocol === 'https:' &&\n      /^[a-z0-9-]+\\.fa\\.(?:[a-z0-9-]+\\.)?(?:ocs\\.)?oraclecloud(?:[1-9][0-9]?)?\\.com$/i.test(parsed.hostname);\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  await oracleProvider.fetch(entry, ctx);\n} catch (e) {\n  if (String(e.message).startsWith('oraclecloud: untrusted hostname')) {\n    logger.warn({ entry: entry.name, url: entry.careers_url }, 'careers_url not on Oracle allowlist — check portals.yml');\n    return null; // skip entry, keep scanning\n  }\n  throw e;\n}","preventionTips":["Validate every portals.yml careers_url against the provider's host regex at config load time, before any scan starts.","Run audit-portals.mjs after editing portals.yml to catch entries no provider claims.","Never point careers_url at vanity/redirect domains; use the tenant's ATS-native hostname.","Add a CI check that runs the provider's assert function against all entries in portals.yml.","Copy URLs from the address bar including the https:// scheme."],"tags":["ssrf-guard","url-validation","config","oracle"],"backgroundTag":"invalid-url","analyzedSha":"e7abd431fce9348a95261acac9e0c14779c35df8","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}