Skip to main content

Common Misconfiguration

Exposed GitHub tokens can lead to unauthorized repository access, code theft, and supply chain attacks. 😱

Vulnerable Example

# VULNERABLE - GitHub Actions workflow with hardcoded token
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          # Never hardcode tokens!
          token: ghp_1234567890abcdefghijklmnopqrstuvwxyzAB
      
      - name: Push to another repo
        run: |
          # Hardcoded PAT in script
          git clone https://ghp_1234567890abcdefghijklmnopqrstuvwxyzAB@github.com/org/private-repo.git
          cd private-repo
          # ... make changes
          git push
// VULNERABLE - Hardcoded GitHub App credentials
const { Octokit } = require("@octokit/rest");
const { createAppAuth } = require("@octokit/auth-app");

// Never hardcode these!
const GITHUB_APP_ID = "123456";
const GITHUB_PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA1234567890abcdefghijklmnop
// ... rest of private key
-----END RSA PRIVATE KEY-----`;
const GITHUB_CLIENT_SECRET = "1234567890abcdef1234567890abcdef12345678";
const GITHUB_WEBHOOK_SECRET = "my_webhook_secret_123";

// Personal Access Token
const GITHUB_TOKEN = "ghp_1234567890abcdefghijklmnopqrstuvwxyzAB";

const octokit = new Octokit({
  auth: GITHUB_TOKEN
});

Secure Example

# SECURE - Using GitHub Secrets and GITHUB_TOKEN
name: Secure Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    # Define minimal permissions for the GITHUB_TOKEN
    permissions:
      contents: read     # Needed for checkout
      packages: write    # Example: Needed to push packages
    
    steps:
      - uses: actions/checkout@v3
        # No 'with: token:' needed here; checkout action uses GITHUB_TOKEN by default

      - name: Configure git (if pushing)
        run: |
          git config --global user.name "github-actions[bot]"
          git config --global user.email "github-actions[bot]@users.noreply.github.com"
      
      - name: Use external service (e.g., deploy to cloud)
        env:
          # Use a custom secret stored in GitHub repo/org settings
          EXTERNAL_TOKEN: ${{ secrets.EXTERNAL_API_TOKEN }} 
        run: |
          # Token is available as an environment variable, not in code
          ./deploy.sh
      
      - name: Create GitHub App token (for advanced cross-repo access)
        id: app-token
        uses: actions/create-github-app-token@v1
        with:
          # Store App credentials securely as secrets
          app-id: ${{ secrets.APP_ID }}
          private-key: ${{ secrets.APP_PRIVATE_KEY }}
      
      - name: Use App token
        env:
          # Use the generated short-lived App token
          GH_TOKEN: ${{ steps.app-token.outputs.token }} 
        run: |
          # Example: Using GitHub CLI with the App token
          gh api /repos/${{ github.repository }}
// SECURE - Using environment variables and secure storage
const { Octokit } = require("@octokit/rest");
const { createAppAuth } = require("@octokit/auth-app");
const fs = require('fs').promises;

class GitHubClient {
    constructor() {
        this.octokit = null;
    }

    async initializeWithToken() {
        // Read token from environment (injected securely)
        const token = process.env.GITHUB_TOKEN;
        
        if (!token) {
            throw new Error('GITHUB_TOKEN not configured');
        }
        
        // Validate token format (optional but recommended)
        if (!this.isValidToken(token)) {
            console.warn('Warning: GitHub token format might be invalid');
        }
        
        this.octokit = new Octokit({
            auth: token,
            userAgent: 'MyApp/1.0.0',
            timeZone: 'UTC',
            baseUrl: 'https://api.github.com' // Use default or GitHub Enterprise URL
        });
        
        // Verify token works by making a simple API call
        try {
            await this.octokit.rest.users.getAuthenticated();
            console.log("GitHub token authenticated successfully.");
        } catch (error) {
            throw new Error(`GitHub token authentication failed: ${error.message}`);
        }
    }

    async initializeAsApp() {
        // Read App credentials from secure environment/files
        const appId = process.env.GITHUB_APP_ID;
        const privateKeyPath = process.env.GITHUB_APP_KEY_PATH; // Path to the .pem file
        const installationId = process.env.GITHUB_APP_INSTALLATION_ID; // Optional, depends on use case
        
        if (!appId || !privateKeyPath) {
            throw new Error('GitHub App credentials not configured (APP_ID, APP_KEY_PATH)');
        }
        
        // Read private key securely
        let privateKey;
        try {
             privateKey = await fs.readFile(privateKeyPath, 'utf8');
        } catch (error) {
             throw new Error(`Failed to read GitHub App private key: ${error.message}`);
        }
        
        this.octokit = new Octokit({
            authStrategy: createAppAuth,
            auth: {
                appId: appId,
                privateKey: privateKey,
                installationId: installationId // May fetch dynamically if needed
            }
        });
         // Add verification if needed, e.g., fetching app info
    }

    isValidToken(token) {
        // GitHub token patterns (fine-grained and classic)
        const patterns = [
            /^ghp_[a-zA-Z0-9]{36}$/, // Personal access token (Classic)
            /^gho_[a-zA-Z0-9]{36}$/, // OAuth access token
            /^ghu_[a-zA-Z0-9]{36}$/, // User-to-server token
            /^ghs_[a-zA-Z0-9]{36}$/, // Server-to-server token
            /^ghr_[a-zA-Z0-9]{36}$/, // Refresh token
            /^github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}$/ // Fine-grained personal access token
        ];
        
        return patterns.some(pattern => pattern.test(token));
    }

    async verifyWebhook(payloadString, signature) {
        // Read webhook secret securely from environment
        const secret = process.env.GITHUB_WEBHOOK_SECRET;
        if (!secret) {
             throw new Error("Webhook secret not configured");
        }
        const crypto = require('crypto');
        
        const hmac = crypto.createHmac('sha256', secret); // GitHub uses sha256 now
        const digest = Buffer.from(`sha256=${hmac.update(payloadString).digest('hex')}`, 'utf8');
        const receivedSig = Buffer.from(signature, 'utf8');

        // Use timingSafeEqual for security
        return crypto.timingSafeEqual(digest, receivedSig);
    }
}
# SECURE - Using Docker BuildKit secrets (example)
# syntax=docker/dockerfile:1.4
FROM node:18-alpine

# Mount secret during build, but don't bake it into the image layer
RUN --mount=type=secret,id=github_token \
    export GITHUB_TOKEN=$(cat /run/secrets/github_token) && \
    npm install --production --ignore-scripts

# Build command:
# docker build --secret id=github_token,src=my_github_token.txt .

# --- OR ---

# Inject secret at runtime using Docker secrets
FROM node:18-alpine
# Copy app code...
# Define how the app reads the secret file
ENV GITHUB_TOKEN_FILE=/run/secrets/github-token
CMD ["node", "app.js"] 
# Run container with: 
# docker run --secret source=my_gh_token_secret,target=github-token ... myapp:latest

Detection Patterns

  • GitHub Personal Access Token (Classic): `ghp_[a-zA-Z0-9]{36}`
  • GitHub Personal Access Token (Fine-Grained): `github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}`
  • GitHub OAuth Access Token: `gho_[a-zA-Z0-9]{36}`
  • GitHub App User-to-Server Token: `ghu_[a-zA-Z0-9]{36}`
  • GitHub App Server-to-Server Token: `ghs_[a-zA-Z0-9]{36}`
  • GitHub App Refresh Token: `ghr_[a-zA-Z0-9]{36}`

Prevention Best Practices

  1. Prefer GITHUB_TOKEN in Actions: For most workflow tasks within the same repository (like checkout, commenting on PRs, uploading artifacts), use the built-in, short-lived GITHUB_TOKEN. It’s automatically available and requires no setup. Define minimal permissions for it using the permissions key.
  2. Use GitHub Secrets for Custom Tokens: If you need to access other repositories, external services, or require higher privileges than GITHUB_TOKEN allows, store Personal Access Tokens (PATs) or service keys in GitHub Encrypted Secrets (at the repository, organization, or environment level). Access them via ${{ secrets.MY_SECRET_NAME }}. Never hardcode them.
  3. Prefer GitHub Apps over PATs: For automation or integrations, especially cross-repository or organization-level tasks, create a GitHub App. Apps have more granular permissions, use short-lived installation tokens (generated via a private key), and are generally more secure and manageable than long-lived PATs tied to a user account.
  4. Implement Token Rotation: All static tokens, especially PATs, should have a defined lifespan. Regularly rotate (delete and create new) your tokens to limit the window of opportunity if one is leaked. GitHub’s fine-grained PATs can have expiration dates.
  5. Enforce Least Privilege: Whether using GITHUB_TOKEN, PATs, or App tokens, grant only the absolute minimum permissions required for the task. Avoid overly broad scopes like repo or admin:org. Use fine-grained PATs or specific App permissions.
  6. Use Fine-Grained PATs: When you must use a PAT, prefer the newer fine-grained tokens over classic ones. Fine-grained tokens allow you to specify repository access and much more granular permissions (e.g., read-only access to code, write access only to issues).
  7. Enable SSO/SAML for Org Tokens: If your organization uses SAML Single Sign-On, require PATs and SSH keys to be authorized for SSO access. This links their validity to the user’s active session.
  8. Monitor Audit Logs: Regularly review GitHub’s audit logs (organization and enterprise levels) for suspicious token usage, creation of new high-privilege tokens, or unexpected API activity.
  9. Verify Webhook Signatures: If your application receives webhooks from GitHub, always configure a webhook secret and verify the X-Hub-Signature-256 header on every incoming request. This ensures the request genuinely came from GitHub and wasn’t forged by an attacker.