Back to Blog
critical SEVERITY8 min read

How GitHub token exposure happens in TypeScript CLI utilities and how to fix it

A critical credential exposure vulnerability was discovered in `cli/src/utils/github.ts`, where three GitHub API fetch calls were made without any safe token-loading mechanism, risking accidental hardcoding or token leakage in logs and CI/CD pipelines. The fix introduces a centralized `getAuthHeaders()` function that reads the token exclusively from the `GITHUB_TOKEN` environment variable and safely injects it into all outbound API requests. This ensures credentials never touch source code, buil

O
By Orbis AppSec
Published June 25, 2026Reviewed June 25, 2026

Answer Summary

This vulnerability is a credential exposure issue (CWE-312/CWE-798) in TypeScript, where GitHub API tokens used in `cli/src/utils/github.ts` lacked a safe, centralized loading mechanism — creating risk of hardcoding or leaking tokens in CI/CD logs. The fix adds a `getAuthHeaders()` function that reads the token only from `process.env['GITHUB_TOKEN']` and spreads it into all three fetch call headers, ensuring the credential never appears in source code or error output.

Vulnerability at a Glance

cweCWE-312 (Cleartext Storage of Sensitive Information) / CWE-798 (Use of Hard-coded Credentials)
fixAdded `getAuthHeaders()` function reading from `process.env['GITHUB_TOKEN']` and spread into all three fetch call headers
riskGitHub API tokens exposed in source code, build artifacts, or CI/CD logs; potential unauthorized repository access
languageTypeScript (Node.js CLI)
root causeThree fetch calls to the GitHub API had no centralized, safe mechanism to load authentication tokens from environment variables
vulnerabilityGitHub API Token Exposure / Credential Leakage

How GitHub Token Exposure Happens in TypeScript CLI Utilities and How to Fix It

Introduction

The cli/src/utils/github.ts file is the backbone of a CLI tool's release management — it fetches releases, retrieves the latest version, and downloads binaries from GitHub. But a security assessment revealed a significant problem: all three of its outbound GitHub API calls lacked a safe, centralized mechanism for loading authentication tokens. This left the door open for tokens to be hardcoded directly into the source, leaked through error output, or silently omitted from requests in ways that could expose rate-limited or private repository data to unauthenticated callers.

This post breaks down exactly what the vulnerability was, how it could be exploited, and what the fix looks like — with real code from the pull request.


The Vulnerability Explained

What Was Missing

At lines 37, 56, and 73 of cli/src/utils/github.ts, three fetch calls are made to the GitHub API:

  • fetchReleases() — fetches all releases from the repo
  • getLatestRelease() — retrieves the most recent release
  • downloadRelease() — downloads a release binary to disk

Before the fix, each of these functions constructed its own headers object inline, with no shared mechanism for injecting an auth token:

// BEFORE — fetchReleases() at line 37
const response = await fetch(url, {
  headers: {
    'Accept': 'application/vnd.github.v3+json',
    'User-Agent': 'uipro-cli',
    // ⚠️ No Authorization header — token handling undefined
  },
});

The same pattern was repeated in getLatestRelease() and downloadRelease(). The security assessment confirmed these calls exist but could not confirm whether a GitHub token was being loaded safely from environment variables or hardcoded somewhere upstream. That ambiguity is itself the vulnerability.

Why This Is Dangerous

When there's no centralized, explicit token-loading function, developers filling in the "missing piece" often reach for the path of least resistance:

  1. Hardcoding the token directly in the source file or a config constant
  2. Interpolating the token into a URL or log string where it becomes visible in CI/CD output
  3. Leaving it out entirely, causing unauthenticated requests that hit GitHub's rate limits and fail silently in production

A hardcoded token in a TypeScript CLI file is particularly dangerous because:
- The CLI is often open-source or distributed as a package, meaning the source is publicly readable
- Build artifacts (compiled JS bundles) may be uploaded to npm or GitHub Releases, carrying the token with them
- CI/CD logs frequently echo environment setup steps, and a poorly written token injection can print the value to stdout

Example Attack Scenario

Imagine a developer adds the token like this to "just make it work":

// Dangerous pattern — token hardcoded or echoed
const TOKEN = 'ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';

const response = await fetch(url, {
  headers: {
    'Authorization': `Bearer ${TOKEN}`,
    'Accept': 'application/vnd.github.v3+json',
    'User-Agent': 'uipro-cli',
  },
});

If this code is committed and pushed — even briefly — the token is now in git history. An attacker with read access to the repository (or a public fork) can extract it, use it to access private repositories, create releases, delete tags, or exfiltrate code. GitHub's secret scanning may catch it, but by then the token has already been exposed.


The Fix

The getAuthHeaders() Function

The fix introduces a single, centralized function that safely reads the GitHub token from the environment and returns it as an Authorization header — or returns an empty object if no token is set:

// AFTER — new function added above fetchReleases()
function getAuthHeaders(): Record<string, string> {
  const token = process.env['GITHUB_TOKEN'];
  return token ? { 'Authorization': `Bearer ${token}` } : {};
}

This is a clean, safe pattern for three reasons:

  1. Single source of truth: The token is loaded in exactly one place. If the loading logic ever needs to change (e.g., to support a secrets manager), you change one function, not three call sites.
  2. Bracket notation for env access: Using process.env['GITHUB_TOKEN'] instead of process.env.GITHUB_TOKEN is a subtle but meaningful choice — it makes the key a string literal that static analysis tools can track, and it avoids accidental property access issues in strict TypeScript configs.
  3. Graceful degradation: If GITHUB_TOKEN is not set, the function returns {}, so the spread operator adds nothing to the headers. The request proceeds unauthenticated rather than throwing an error — appropriate for public repository access.

Before and After: All Three Call Sites

fetchReleases() (line 43):

// BEFORE
headers: {
  'Accept': 'application/vnd.github.v3+json',
  'User-Agent': 'uipro-cli',
},

// AFTER
headers: {
  'Accept': 'application/vnd.github.v3+json',
  'User-Agent': 'uipro-cli',
  ...getAuthHeaders(),
},

getLatestRelease() (line 63):

// BEFORE
headers: {
  'Accept': 'application/vnd.github.v3+json',
  'User-Agent': 'uipro-cli',
},

// AFTER
headers: {
  'Accept': 'application/vnd.github.v3+json',
  'User-Agent': 'uipro-cli',
  ...getAuthHeaders(),
},

downloadRelease() (line 81):

// BEFORE
headers: {
  'User-Agent': 'uipro-cli',
  'Accept': 'application/octet-stream',
},

// AFTER
headers: {
  'User-Agent': 'uipro-cli',
  'Accept': 'application/octet-stream',
  ...getAuthHeaders(),
},

The spread operator (...getAuthHeaders()) is elegant here — it conditionally injects the header only when the token exists, keeping the code clean and avoiding undefined header values that some HTTP clients handle inconsistently.


Prevention & Best Practices

1. Always Centralize Credential Loading

Never scatter process.env['TOKEN'] calls across multiple functions. Create a single auth utility (like getAuthHeaders()) and import it wherever needed. This makes auditing trivial — one grep for the function name shows you every place credentials are used.

2. Use Environment Variables — And Validate Them at Startup

// Optional: validate required tokens at CLI startup
function validateEnv(): void {
  if (!process.env['GITHUB_TOKEN']) {
    console.warn('Warning: GITHUB_TOKEN not set. API rate limits apply.');
  }
}

For private repository access where the token is mandatory, fail fast with a clear error rather than making unauthenticated requests that will fail with a cryptic 404.

3. Never Log Tokens — Even Partially

Avoid patterns like:

// ❌ Dangerous — token appears in logs
console.error(`Request failed with token: ${token?.substring(0, 8)}...`);

Even partial tokens can aid attackers in narrowing down brute-force attempts.

4. Use a Secrets Manager for Production

For CI/CD pipelines, use GitHub Actions secrets, AWS Secrets Manager, or HashiCorp Vault rather than plain environment variables in .env files that might be committed accidentally.

5. Scan Your Repository

  • GitLeaks: Detects hardcoded secrets in git history
  • GitHub Secret Scanning: Automatically revokes exposed GitHub tokens
  • Semgrep: Can detect unsafe process.env patterns and missing auth headers

Relevant Standards

  • CWE-312: Cleartext Storage of Sensitive Information
  • CWE-798: Use of Hard-coded Credentials
  • OWASP A02:2021: Cryptographic Failures (which includes credential exposure)
  • OWASP A07:2021: Identification and Authentication Failures

Key Takeaways

  • The absence of a token-loading mechanism is itself a vulnerability — in github.ts, three API call sites had no shared, safe way to inject auth credentials, creating a gap that could be filled dangerously.
  • Centralizing credential access in getAuthHeaders() eliminates three separate risk points — instead of auditing three fetch calls, security reviewers now audit one function.
  • Bracket notation process.env['GITHUB_TOKEN'] is preferable to dot notation in TypeScript for environment variable access, as it plays better with static analysis and strict type checking.
  • Graceful degradation matters — returning {} when no token is set means unauthenticated requests proceed for public repos, rather than crashing the CLI with an unhelpful error.
  • Token exposure risk is amplified in CLI tools because they're often open-sourced, distributed as packages, and run in CI/CD environments where logs are broadly accessible.

How Orbis AppSec Detected This

  • Source: GitHub API token — potentially hardcoded or unsafely loaded upstream of the three fetch call sites in cli/src/utils/github.ts
  • Sink: Three fetch() calls at lines 37, 56, and 73, each constructing headers inline with no shared auth-injection mechanism
  • Missing control: No centralized function to safely read GITHUB_TOKEN from environment variables; no guarantee the token wasn't being hardcoded or echoed in error output
  • CWE: CWE-312 (Cleartext Storage of Sensitive Information) and CWE-798 (Use of Hard-coded Credentials)
  • Fix: Added getAuthHeaders() function reading exclusively from process.env['GITHUB_TOKEN'] and spread into all three fetch call header objects

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

The vulnerability in cli/src/utils/github.ts is a textbook example of how credential exposure often isn't a single dramatic mistake — it's the absence of a safe pattern that creates space for dangerous ones. Three GitHub API calls with no centralized auth mechanism left the codebase one bad commit away from a hardcoded token or a leaked secret in CI logs.

The fix is small — 11 lines added — but architecturally significant. By introducing getAuthHeaders() as the single source of truth for GitHub authentication, the code is now auditable, safe, and extensible. Any future GitHub API calls added to this file will naturally reach for the same function, carrying the secure pattern forward.

For developers building CLI tools that interact with external APIs: credential loading is not a detail to handle ad-hoc at each call site. Treat it as infrastructure, centralize it early, and let static analysis tools verify it stays safe.


References

Frequently Asked Questions

What is GitHub token exposure in TypeScript CLI tools?

It occurs when API tokens used to authenticate GitHub API calls are either hardcoded in source files or lack a safe loading mechanism, making them visible in code repositories, build logs, or error output.

How do you prevent GitHub token exposure in TypeScript?

Always load tokens from environment variables using `process.env['TOKEN_NAME']`, centralize token access in a single function, and never interpolate tokens directly into strings that appear in logs or error messages.

What CWE is GitHub token exposure?

It maps to CWE-312 (Cleartext Storage of Sensitive Information) and CWE-798 (Use of Hard-coded Credentials), depending on whether the token is stored in plaintext or embedded directly in code.

Is using environment variables enough to prevent token exposure?

Environment variables are necessary but not sufficient on their own — you must also ensure tokens are never logged, never echoed in error output, and that the environment itself is secured (e.g., using secrets managers in CI/CD pipelines).

Can static analysis detect GitHub token exposure?

Yes. Tools like Semgrep, GitLeaks, and multi-agent AI scanners (like Orbis AppSec) can detect patterns where API calls lack proper credential-loading mechanisms or where tokens may be interpolated into loggable strings.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #294

Related Articles

critical

How arbitrary code execution via injected protobuf definition type fields happens in Node.js and how to fix it

A critical arbitrary code execution vulnerability (CVE-2026-41242) was discovered in protobufjs versions prior to 7.5.5 and 8.0.1, allowing attackers to inject malicious code through crafted protobuf definition type fields. The fix upgrades the dependency from the vulnerable version 6.11.4 to 7.5.5, which properly sanitizes type field inputs during protobuf parsing. This vulnerability is especially dangerous because protobufjs is widely used in Node.js applications for serialization, meaning a s

high

How command injection happens in Node.js child_process and how to fix it

A high-severity command injection vulnerability was discovered in `bootstrap.js` at line 34, where the `exec()` function was used to run shell commands constructed from a `folder` variable. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates the shell interpolation entirely, closing the door on command injection attacks that could have affected all downstream consumers of this Node.js library.

high

How unsafe pickle deserialization happens in NumPy's np.load() and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `tools/ardy-engine/retarget.py` where `np.load()` was called with `allow_pickle=True`, enabling attackers to embed malicious pickle payloads in `.npz` files. The fix was a single-character change—switching `allow_pickle=True` to `allow_pickle=False`—that eliminates the deserialization attack vector while preserving the file's legitimate array data loading functionality.

high

How SSRF and Credential Leakage via Absolute URLs happens in axios and how to fix it

A high-severity vulnerability (CVE-2025-27152) in axios versions prior to 1.8.2 allowed Server-Side Request Forgery (SSRF) attacks and credential leakage when making HTTP requests with absolute URLs. This vulnerability was fixed by upgrading from axios 1.7.4 to 1.8.2 in both package.json and package-lock.json, eliminating the attack vector that could have exposed authentication tokens and enabled unauthorized server-side requests.

high

How pickle-based arbitrary code execution happens in PyTorch and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `scripts/export_joyvasa_audio.py` where `torch.load()` was called with `weights_only=False`, allowing any pickle-serialized Python object — including malicious code — to execute during checkpoint loading. The fix switches to `weights_only=True` and explicitly allowlists only the two non-standard classes the checkpoint actually requires: `argparse.Namespace` and `pathlib.PosixPath`. This closes a real code execution path tha

critical

How WebSocket protocol length header abuse happens in Node.js and how to fix it

A critical vulnerability (CVE-2026-54466) in websocket-driver versions prior to 0.7.5 allows attackers to corrupt WebSocket messages by manipulating protocol length headers. This can lead to data integrity issues, denial of service, or potentially arbitrary code execution in affected Node.js applications. The fix involves upgrading the websocket-driver dependency to version 0.7.5.