{"record":{"id":"98901aed094a6464","repo":"vercel/turborepo","slug":"github-token-gh-token-contains-invalid-characters","errorCode":null,"errorMessage":"GITHUB_TOKEN/GH_TOKEN contains invalid characters, ignoring.","messagePattern":"GITHUB_TOKEN/GH_TOKEN contains invalid characters, ignoring\\.","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/turbo-utils/src/examples.ts","lineNumber":31,"sourceCode":"\nconst REQUEST_TIMEOUT = 10000;\nconst DOWNLOAD_TIMEOUT = 120000;\n\n// Hosts that receive Authorization headers when GITHUB_TOKEN / GH_TOKEN is set.\n// Limited to GitHub.com API hosts. GitHub Enterprise Server is not yet supported.\nconst GITHUB_API_HOSTS = new Set([\"api.github.com\", \"codeload.github.com\"]);\n\n/**\n * Reads a GitHub personal access token from the environment.\n * GITHUB_TOKEN takes precedence over GH_TOKEN, matching the GitHub CLI convention.\n * Requires `repo` scope (classic PAT) or `contents:read` (fine-grained PAT).\n */\nfunction getGitHubToken(): string | undefined {\n  const token = (process.env.GITHUB_TOKEN || process.env.GH_TOKEN || \"\").trim();\n  if (!token) return undefined;\n  // eslint-disable-next-line no-control-regex -- Intentional: reject tokens containing control characters\n  if (/[\\r\\n\\u0000]/.test(token)) {\n    warn(\"GITHUB_TOKEN/GH_TOKEN contains invalid characters, ignoring.\");\n    return undefined;\n  }\n  return token;\n}\n\n/**\n * Returns an Authorization header for GitHub API requests when a token\n * is available. Only sends tokens to hosts in GITHUB_API_HOSTS to\n * prevent credential leakage to third-party domains.\n */\nfunction getGitHubAuthHeaders(url: string): Record<string, string> {\n  try {\n    const { hostname } = new URL(url);\n    if (!GITHUB_API_HOSTS.has(hostname)) {\n      return {};\n    }\n  } catch {\n    return {};","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/vercel/turborepo/blob/f9245100cf0d31d96628804ead485f6bf226e55a/packages/turbo-utils/src/examples.ts#L13-L49","documentation":"The turbo-utils example downloader reads a GitHub token from GITHUB_TOKEN (or GH_TOKEN) to authenticate requests to api.github.com/codeload.github.com. Before use, the token is validated against /[\\r\\n\\u0000]/; if it contains a carriage return, line feed, or NUL character, the token is considered malformed and is silently discarded (this message is only a warning). The download then proceeds unauthenticated, which can surface later as GitHub rate-limit or auth-failed warnings. The check exists because control characters in an Authorization header enable header-injection and are never part of a valid PAT.","triggerScenarios":"getGitHubToken() returns non-empty process.env.GITHUB_TOKEN or GH_TOKEN whose value contains \\r, \\n, or \\u0000 — e.g. a CI secret saved with a trailing newline, a Windows CRLF-terminated .env line, a multi-line pasted value, or a value quoted across lines in a shell profile. Then getGitHubAuthHeaders()/isUrlOk/downloadAndExtractExample log this warning and send no Authorization header.","commonSituations":"A GitHub Actions/Docker secret defined with a trailing newline (classic on Windows runners or editors adding CRLF); an .env file written on Windows and committed with CRLF line endings; a token copied with an embedded line break from a password manager; exporting the token with `echo token >> ~/.bashrc` so a stray newline lands inside the value.","solutions":["Re-set the secret/environment value with no trailing newline: `printf '%s' 'ghp_xxx'` instead of echo, or re-paste the PAT into the CI secret field and confirm no line break.","If using an .env file created on Windows, convert line endings (dos2unix) or rewrite the file so GITHUB_TOKEN sits on a single LF-terminated line.","Verify the token is clean before running: `node -e \"const t=process.env.GITHUB_TOKEN||process.env.GH_TOKEN;console.log(/[\\r\\n\\u0000]/.test(t||''))\"` — it must print false.","Regenerate the PAT at github.com/settings/tokens if the stored value itself got corrupted, and update the secret."],"exampleFix":"# before (adds trailing newline -> token rejected)\necho ghp_xxxxxxxx >> ~/.bashrc\nexport GITHUB_TOKEN=\"ghp_xxxxxxxx\\n\"\n\n# after (value stays on one line, no newline inside)\nprintf 'export GITHUB_TOKEN=%s\\n' 'ghp_xxxxxxxx' >> ~/.bashrc\nexport GITHUB_TOKEN=ghp_xxxxxxxx","handlingStrategy":"validation","validationCode":"const TOKEN_RE = /^[A-Za-z0-9_-]+$/; // ghp_/github_pat_ tokens are URL-safe, no newlines/NULs\nconst raw = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || \"\";\nif (raw && !TOKEN_RE.test(raw.trim())) {\n  console.error(\"GITHUB_TOKEN/GH_TOKEN has invalid characters; re-set the secret without newlines.\");\n  process.exit(1); // fail fast before create-turbo runs\n}","typeGuard":"function isValidGitHubToken(value: string | undefined): value is string {\n  if (!value) return false;\n  const t = value.trim();\n  return t.length > 0 && !/[\\r\\n\\u0000]/.test(t);\n}","tryCatchPattern":null,"preventionTips":["Never create token secrets with echo >>; use printf or the CI UI so no trailing newline is embedded.","Add a preflight check in CI that fails the job if /[\\r\\n\\u0000]/ matches the token, instead of silently losing auth.","Keep .env files LF-only (editorconfig insert_final_newline applies to the file, not values); run dos2unix on files authored on Windows.","Note the library only warns: an ignored token degrades to unauthenticated GitHub requests and can later look like rate-limiting — check for this warning first."],"tags":["github","environment-variables","authentication","token","ci"],"backgroundTag":"invalid-api-token","analyzedSha":"f9245100cf0d31d96628804ead485f6bf226e55a","analyzedAt":"2026-08-17T10:46:15.696Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}