ByteTools Logo

GitHub Security Guide

Secrets, repos, PATs, and integration risks

For developers who want to stop leaking credentials before they get exploited

Why GitHub is a High-Value Target

Your code repository is not just source code — it is a map of your infrastructure. Secrets committed to git, overprivileged tokens, misconfigured repo settings, and third-party integrations with excessive access are among the most common causes of real-world breaches.

Automated bots continuously scrape GitHub for API keys, tokens, and credentials — both in public repos and via leaked tokens that grant access to private ones. A secret exposed for 60 seconds is enough to be discovered and abused.

This guide covers six attack surfaces: client-side secret exposure, git history leaks, repo misconfigurations, GitHub Actions risks, PAT overprovisioning, and third-party integration abuse.

1. Client-Side Secret Exposure

Framework-specific traps that ship your secrets to every browser

Modern frontend frameworks have a concept of build-time environment variables — values that get embedded directly into your compiled JavaScript bundle and sent to every user's browser. This is by design for things like your public analytics ID. It is catastrophic for API keys, database URLs, or any secret.

AI-generated code frequently gets this wrong because it doesn't distinguish between server-side and client-side execution contexts.

Framework Danger Zones

Next.js — NEXT_PUBLIC_ prefix = exposed to browser

# .env
NEXT_PUBLIC_STRIPE_KEY=sk_live_...   ← WRONG: bundled into client JS
NEXT_PUBLIC_OPENAI_KEY=sk-proj-...  ← WRONG: visible in DevTools

# Correct — no NEXT_PUBLIC_ prefix = server only
STRIPE_SECRET_KEY=sk_live_...
OPENAI_API_KEY=sk-proj-...

Vite / React — VITE_ prefix = exposed to browser

# .env
VITE_API_SECRET=abc123...    ← WRONG: bundled into client
VITE_DB_PASSWORD=hunter2     ← WRONG: visible in page source

# Private vars in Vite must NOT have the VITE_ prefix
# and must only be read in server-side code

Create React App — REACT_APP_ prefix = exposed to browser

# Everything prefixed REACT_APP_ is public
REACT_APP_FIREBASE_API_KEY=...   ← in the bundle
REACT_APP_TWILIO_AUTH_TOKEN=...  ← in the bundle

The Correct Pattern: Proxy Through Your Server

The fix is always the same regardless of framework: the browser should never call a third-party API directly with a secret. Route the call through your own API endpoint, which holds the secret server-side.

Wrong — browser holds the key

// React component
const res = await fetch(
  "https://api.openai.com/v1/chat",
  {
    headers: {
      Authorization: `Bearer ${
        process.env.REACT_APP_OPENAI_KEY
      }`  // ← in the bundle
    }
  }
);

Correct — server proxies the request

// React component — calls YOUR api
const res = await fetch("/api/chat", {
  method: "POST",
  body: JSON.stringify({ prompt })
});

// /api/chat (server-side)
// process.env.OPENAI_API_KEY lives here
// never touches the browser

Auditing Your Built Bundle

Attackers extract secrets from production bundles using simple string analysis. You can do the same to audit your own build before shipping:

# Build your app first, then search the output
npm run build

# Search for common secret patterns in the bundle
grep -r "sk-" ./dist
grep -r "sk_live" ./dist
grep -r "AKIA" ./dist          # AWS key prefix
grep -r "Bearer " ./dist
grep -r "password" ./dist

# Or use trufflehog on your built output
trufflehog filesystem ./dist

Rule: Any env var that needs to stay secret must never have the framework's client-prefix (NEXT_PUBLIC_, VITE_, REACT_APP_). Secrets are read on the server only. The browser calls your API, your API calls theirs.

2. Git History — Secrets Are Never Truly Deleted

Deleting a file does not remove it from history

This is one of the most misunderstood aspects of git. When you commit a secret and then delete it in the next commit, the secret is still fully accessible in git history. Anyone with access to the repo — or anyone who cloned it before the deletion — can retrieve it with a single command.

How attackers retrieve deleted secrets

# Find all commits that touched .env or config files
git log --all --full-history -- "**/.env"
git log --all --full-history -- "**/config.js"

# View the content of a specific past commit
git show <commit-hash>:.env

# Search entire history for a string pattern
git log -p | grep -i "api_key|secret|password|token"

The "I deleted it" false sense of security

All of the following leave secrets permanently accessible in history:

  • Deleting the file and committing
  • Overwriting the value with a placeholder
  • Moving the file to .gitignore after it was already tracked
  • Squashing commits (the squashed history is still in the reflog)

If a Secret Was Committed — Immediate Response

# Step 1: Rotate the secret IMMEDIATELY (assume it's already compromised)
# Do this before anything else — history rewriting takes time

# Step 2: Remove secret from history using git-filter-repo
pip install git-filter-repo
git filter-repo --path .env --invert-paths

# Or remove a specific string from all history
git filter-repo --replace-text <(echo 'sk-proj-abc123==>REMOVED')

# Step 3: Force push all branches (coordinate with team first)
git push origin --force --all

# Step 4: Invalidate all existing clones
# Anyone who cloned before must re-clone — old clones still have the history

Prevention: .gitignore Before First Commit

# Create .gitignore BEFORE git init or first commit
# Minimum entries for any project:

.env
.env.local
.env.*.local
.env.production
*.pem
*.key
secrets/
credentials.json
service-account.json

Rule: If a secret was ever committed — even briefly, even in a private repo — treat it as compromised and rotate it immediately. Rewriting history is a secondary cleanup step, not the primary response. Enable GitHub Secret Scanning (Settings → Security → Secret scanning) to be alerted automatically when patterns are detected.

3. Repo Misconfiguration

Default GitHub settings are not secure defaults

Branch Protection Rules

Without branch protection on main, any contributor — or an attacker with a compromised token — can push directly, bypass CI, and deploy malicious code.

Recommended branch protection settings for main

Require pull request before merging

No direct pushes to main — all changes go through a PR

Require at least 1 approving review

Someone else must review before merge

Dismiss stale pull request approvals

New commits reset approval — prevents sneak changes after approval

Require status checks to pass

CI must pass (tests, lint, build) before merge

Require branches to be up to date

PR branch must be current with main before merge

Do not allow bypassing above settings

Even admins must follow the rules

Restrict who can push to matching branches

Limit direct push access to a specific team or no one

Repo Visibility

Making a repo public is irreversible in terms of data exposure — if a secret was in the repo before it was made public, bots may have already scraped it. Additionally, even "private" repos at the free tier can become public accidentally or through permission misconfiguration.

Before making a repo public

  • Scan entire git history for secrets
  • Run trufflehog git file://.
  • Review all environment variable references
  • Check for internal hostnames, IPs, or internal URLs
  • Remove any employee names or internal project names

Org-level settings to review

  • Disable forking of private repos
  • Restrict repo creation to admins only
  • Require 2FA for all org members
  • Restrict base permissions to Read (not Write)
  • Review and prune outside collaborators regularly

4. GitHub Actions Security

CI pipelines are a common lateral movement path

The Fork PR Attack

By default, workflows triggered by pull_request from forks do not have access to secrets. But pull_request_target runs in the context of the base repo and does — meaning a malicious fork PR can exfiltrate all your CI secrets if your workflow is misconfigured.

Dangerous pattern

on:
  pull_request_target:  # ← has secrets
    types: [opened]

jobs:
  build:
    steps:
      - uses: actions/checkout@v4
        with:
          # Checking out fork code with
          # access to secrets = RCE risk
          ref: ${{ github.event.pull_request.head.sha }}

Safe for PRs from forks

on:
  pull_request:  # ← no secrets for forks
    types: [opened, synchronize]

# For deploy workflows that need secrets,
# use workflow_run triggered after
# pull_request completes — never give
# fork code direct access to secrets

GITHUB_TOKEN Least Privilege

The GITHUB_TOKEN available in every workflow defaults to broad write permissions in many repos. Scope it to the minimum your workflow actually needs.

# Set default permissions to read-only for the whole workflow
permissions: read-all

jobs:
  deploy:
    permissions:
      contents: read        # only what's needed
      id-token: write       # for OIDC auth if required
      # everything else: none (not declared = denied)

Pin Third-Party Actions to a Commit SHA

uses: actions/checkout@v4 means "whatever the v4 tag points to today." Tag references can be moved by the action maintainer (or an attacker who compromises their account) to point to malicious code. Pin to a specific commit SHA instead.

Tag reference — mutable

- uses: actions/checkout@v4
- uses: actions/setup-node@v4

SHA pin — immutable

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af  # v4.1.0

Rule: Default permissions: read-all on every workflow. Never use pull_request_target with fork code checkout. Pin third-party actions to commit SHAs. Enable Dependabot for Actions to get automated SHA update PRs.

5. PAT Hygiene — Fine-Grained, Scoped, Expiring

A single overprivileged token can hand an attacker your entire org

Personal Access Tokens (PATs) are a major risk surface because developers tend to create them once, give them broad permissions "just in case," set no expiry, and then use them across multiple systems. When one of those systems is compromised, the attacker has a token with access to everything.

Classic PAT — what not to do

  • Scope: repo (full read/write all repos)
  • Scope: admin:org (manage the whole org)
  • Expiration: No expiration
  • Usage: committed to CI env vars, shared in Slack
  • Applies to: all repos in the account

Fine-grained PAT — what to do

  • Scope: Contents: Read (specific repos only)
  • Resource owner: specific org or user
  • Expiration: 30 or 90 days max
  • Usage: stored in a secrets manager, never shared
  • Applies to: one named repo

PAT Permission Reference

Use CaseMinimum Permission NeededNot Needed
Clone / pull a repoContents: ReadWrite, Admin
Push code to a repoContents: Read & WriteAdmin, Delete
Create a PRPull requests: Read & WriteAdmin, org access
Read GitHub Actions logsActions: ReadWrite, Secrets
Trigger a workflow dispatchActions: WriteAdmin, repo:all
Read org member listMembers: ReadAdmin, Write
Deploy via CIContents: Read (use OIDC instead)repo scope on classic PAT

Rule: Always use Fine-grained PATs over Classic PATs. Set a maximum 90-day expiry. Grant access to specific repos only, not all repos. For CI/CD, prefer OIDC (OpenID Connect) over PATs entirely — it issues short-lived tokens per-workflow-run with no long-lived secret to rotate or leak. Audit your active PATs at Settings → Developer settings → Personal access tokens and revoke any you don't recognize or haven't used recently.

6. Third-Party Integrations — Audit & Revoke

Every integration is a potential breach vector

GitHub integrations — OAuth apps, GitHub Apps, and CI/CD connections — accumulate over time. Each one represents a third party that has been granted some level of access to your repos or org. When that third party is compromised (it happens regularly), the attacker inherits whatever permissions you granted them.

Common Overprovisioning Patterns

High

CI/CD tools granted repo scope org-wide

Vercel, Netlify, Railway, and others request access to all repos when you connect them. Switch to installing the GitHub App on specific repos only — most providers support this.

Medium

Code review bots with write access

Some code quality tools request write access to post comments. Read access is sufficient for most. Review what each integration actually does before approving.

High

Bot/service accounts with admin role

Integration accounts created as 'admin' for convenience. These accounts should have the minimum role needed — usually write or maintain, never admin.

Medium

Forgotten OAuth apps from old projects

Apps authorized years ago for a project that no longer exists, still holding live permissions. These accumulate and are rarely audited.

High

Dependabot with write access to protected branches

Dependabot auto-merge enabled on main without requiring review. An attacker who can influence a dependency can merge malicious code.

How to Audit Your Integrations

Where to look in GitHub

Personal integrations:
  Settings → Applications → Authorized OAuth Apps
  Settings → Applications → Authorized GitHub Apps
  Settings → Developer settings → Personal access tokens

Organization integrations:
  Org Settings → Third-party access
  Org Settings → GitHub Apps
  Org Settings → OAuth application policy

For each integration, ask:
  1. Do we still use this?
  2. What permissions did we grant?
  3. Is this the minimum needed?
  4. When was it last active?

Rule: Schedule a quarterly integration audit. Revoke anything you don't recognize or no longer use. When installing new integrations, always choose "Only select repositories" rather than "All repositories." Prefer GitHub Apps over OAuth Apps — they offer finer-grained permissions and better audit trails.

GitHub Security Checklist

Work through this list for every active repository and organization.

Secrets & Client-Side Exposure

Repo Settings

GitHub Actions

PATs & Integrations

The 10-minute quarterly review

Most of these risks accumulate slowly — a PAT created a year ago, an integration installed for a project that ended, a branch protection rule that was temporarily disabled and never re-enabled. Schedule a 10-minute calendar block every quarter to run through the checklist above. That habit alone eliminates the majority of the attack surface described in this guide.

Related Tools & Resources

ByteTools

Secret Scanning Tools

  • • TruffleHog (git history scan)
  • • GitGuardian (real-time monitoring)
  • • GitHub Secret Scanning (built-in)
  • • Gitleaks (pre-commit hook)

Further Reading

  • • GitHub Security Hardening Docs
  • • OWASP Secrets Management Cheat Sheet
  • • Staying Safe with GitHub Actions
  • • OIDC in GitHub Actions Guide

Frequently Asked Questions

How do I prevent secrets from being committed to my GitHub repository?

Add a .gitignore that excludes .env files and credential files before your first commit. Install a pre-commit hook using git-secrets or Gitleaks to block commits containing API keys or passwords. Enable GitHub's built-in secret scanning (free for public repos) to catch anything that slips through. If a secret does get committed, rotate it immediately — just deleting it from history is not enough, assume it is compromised.

What is a fine-grained personal access token and why should I use one?

A fine-grained PAT (personal access token) is a GitHub token that limits access to specific repositories and specific permissions, rather than granting broad access to all your repos. Classic PATs give full read/write access to every repo your account can access — a leak exposes everything. Fine-grained PATs follow the principle of least privilege: a token for one automation can only touch one repo and only perform the operations it needs.

How do I secure GitHub Actions against supply chain attacks?

Pin all third-party GitHub Actions to a specific commit SHA instead of a mutable tag like @v3 — a SHA cannot be silently replaced. Set permissions: write-all to the minimum your workflow needs. Never use pull_request_target for untrusted forks without carefully scoping what it can access. Use OIDC (OpenID Connect) instead of storing long-lived cloud credentials as secrets.

What branch protection rules should every team enable on GitHub?

Essential branch protection rules for main/master: require pull requests with at least one approval, require status checks to pass before merging, require branches to be up to date, prohibit force pushes, and do not allow deletion. For sensitive repos, also enable required signed commits and restrict who can push directly.

How do I audit third-party apps and OAuth integrations on my GitHub account?

Go to GitHub Settings > Applications > Authorized OAuth Apps and Installed GitHub Apps. Revoke access for any app you no longer use. For each remaining app, check what permissions it has and what repos it can access — restrict to specific repos where possible instead of granting access to all repositories. Review this list quarterly, as integrations accumulate over time.