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

high

How improper handling of case sensitivity happens in Go MCP SDK and how to fix it

A high-severity vulnerability (CVE-2026-27896) in the Model Context Protocol Go SDK v1.3.0 allowed attackers to bypass security controls through improper handling of case sensitivity. The fix upgrades the dependency from v1.3.0 to v1.3.1, which correctly normalizes case comparisons. This vulnerability was particularly concerning for CLI tools where attackers could manipulate input to evade validation logic.

high

How buffer overflow via insecure strcpy/strncpy happens in C textbox widgets and how to fix it

A high-severity buffer overflow vulnerability was discovered in the Aroma UI framework's textbox widget where `strncpy()` was used to copy user-provided text without guaranteed null-termination safety. The fix replaces the dangerous `strncpy()` pattern with `snprintf()`, which automatically handles buffer boundaries and null-termination in a single, safer operation.

critical

How buffer overflow via sprintf happens in C++ fuzzer code and how to fix it

A critical buffer overflow vulnerability was discovered in `prog/fuzzing/recog_basic_fuzzer.cc` where `sprintf` writes to a fixed 256-byte buffer without bounds checking. An attacker providing crafted fuzzer input could exploit this to corrupt memory. The fix replaces `sprintf` with `snprintf`, enforcing the buffer size limit and preventing overflow.

critical

How buffer overflow in memcpy happens in C bios_disk.h and how to fix it

A critical buffer overflow vulnerability was discovered in `include/bios_disk.h` at line 474, where a `memcpy` operation copies 512 bytes from a source buffer without properly validating that the calculated offset from the `sectnum` parameter stays within bounds. An attacker controlling the `sectnum` parameter could trigger an out-of-bounds read, potentially leaking sensitive memory contents or causing a crash. The fix adds a proper bounds check before the memcpy call to ensure the source offset

high

How unbounded input size denial-of-service happens in C lexer functions and how to fix it

A high-severity denial-of-service vulnerability was discovered in the PH7 lexer where the `PH7_TokenizePHP()` function accepted arbitrarily large input sizes without validation. An attacker could submit gigabyte-scale PHP code, causing proportional CPU and memory exhaustion. The fix introduces a configurable input size cap enforced before lexer processing begins.

critical

How command injection happens in Python os.system() and how to fix it

A critical command injection vulnerability was discovered in `src/O4_Geotag.py` where file paths and coordinate values were concatenated directly into `os.system()` calls invoking `gdal_translate` and `gdalwarp`. Because `os.system()` passes its argument through a shell interpreter, any shell metacharacters in the file path variable `f` — sourced from file enumeration or user-supplied input — could be exploited to execute arbitrary commands. The fix replaces both shell invocations with direct ca