{"record":{"id":"7c0f91a98b96bdee","repo":"santifer/career-ops","slug":"nofluffjobs-url-must-use-https-url","errorCode":null,"errorMessage":"nofluffjobs: URL must use HTTPS: ${url}","messagePattern":"nofluffjobs: URL must use HTTPS: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/nofluffjobs.mjs","lineNumber":21,"sourceCode":"\n// NoFluffJobs provider — hits the public search posting API.\n// It intentionally returns only the core scanner job fields; richer skill and\n// salary metadata can be added later if the provider contract is expanded.\n\nconst ALLOWED_HOSTS = new Set(['nofluffjobs.com']);\nconst API_URL = 'https://nofluffjobs.com/api/search/posting';\nconst JOB_BASE = 'https://nofluffjobs.com/pl/job/';\nconst PAGE_SIZE = 20;\nconst MAX_PAGES = 5;\n\nfunction assertNoFluffUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`nofluffjobs: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`nofluffjobs: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_HOSTS.has(parsed.hostname)) {\n    throw new Error(`nofluffjobs: untrusted hostname \"${parsed.hostname}\" — must be nofluffjobs.com`);\n  }\n  return parsed;\n}\n\nfunction detectUrl(entry) {\n  const url = entry.api || entry.careers_url || '';\n  if (typeof url !== 'string' || !url.trim()) return null;\n  try {\n    return { url: assertNoFluffUrl(url).href };\n  } catch {\n    return null;\n  }\n}\n\nfunction normalizeLocation(posting) {\n  const parts = [];","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/nofluffjobs.mjs#L3-L39","documentation":"Thrown by nofluffjobs' assertNoFluffUrl() when the URL parses but its protocol is not 'https:'. This is the second SSRF gate preventing plaintext HTTP fetches that are vulnerable to MITM and redirect-based SSRF. Fires before any network request.","triggerScenarios":"A valid URL with an http: (or other non-https) scheme is passed. Typical: entry.api or entry.careers_url in portals.yml prefixed with http:// instead of https://. Also fires in test setups pointing at http://localhost.","commonSituations":"Copy-pasting a URL from a browser address bar that didn't auto-upgrade to HTTPS. A config-management tool or environment variable that strips the scheme or defaults to http. Local development against a non-TLS mock server.","solutions":["Update the URL in portals.yml (api or careers_url) to use https://.","For local testing, run the mock on HTTPS or inject a test-only bypass.","Audit any script that generates portals.yml entries to enforce https:// as the default scheme."],"exampleFix":"// before\napi: 'http://nofluffjobs.com/api/search/posting'\n\n// after\napi: 'https://nofluffjobs.com/api/search/posting'","handlingStrategy":"validation","validationCode":"/** Normalize URL to HTTPS before passing to the provider. */\nfunction ensureHttps(url) {\n  if (typeof url !== 'string') return null;\n  return url.replace(/^http:\\/\\//i, 'https://');\n}\n\nentry.api = ensureHttps(entry.api) || entry.api;","typeGuard":"/** @param {string} url @returns {boolean} */\nfunction isHttpsUrl(url) {\n  try { return new URL(url).protocol === 'https:'; } catch { return false; }\n}","tryCatchPattern":"try {\n  await nofluffProvider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).includes('must use HTTPS')) {\n    entry.api = (entry.api || '').replace(/^http:/i, 'https:');\n    await nofluffProvider.fetch(entry, ctx);\n  } else throw err;\n}","preventionTips":["Author portal URLs with https:// in config.","Lint config files for http:// URLs.","Use HTTPS mock servers in tests."],"tags":["url-validation","ssrf-guard","https","nofluffjobs","config"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}