GitHub Advanced Security Testing: CodeQL, Secret Scanning, and Dependabot Bypass

GitHub Advanced Security (GHAS) provides CodeQL SAST, secret scanning, and Dependabot vulnerability alerts — but each has coverage gaps, bypass techniques, and misconfiguration risks. This guide covers how to audit GHAS deployments: identifying CodeQL blind spots, testing secret scanning effectiveness, exploiting Dependabot auto-merge misconfigurations, and reviewing Actions workflow permissions that undermine the entire security posture.

Table of Contents

  1. GHAS Security Architecture
  2. CodeQL Coverage and Gap Testing
  3. Secret Scanning Bypass Techniques
  4. Dependabot Misconfiguration Exploitation
  5. Actions Workflow Permission Auditing
  6. SARIF Upload Abuse
  7. Organization-Level Security Policy Gaps
  8. GHAS Hardening Recommendations

GHAS Security Architecture

GitHub Advanced Security combines three security layers, each with distinct attack surfaces:

GHAS FeatureFunctionPrimary Attack Surface
CodeQLSAST — finds code vulnerabilities via semantic analysisCoverage gaps, query exclusions, language support limits
Secret ScanningDetects committed secrets matching provider patternsCustom format secrets, encoded/split secrets, bypass techniques
Dependabot AlertsIdentifies vulnerable dependenciesTransitive deps, version pinning, auto-merge abuse
Dependabot Security UpdatesAuto-creates PRs to fix vulnerable depsAuto-merge misconfig, dependency confusion
Push ProtectionBlocks commits with detected secretsBypass flags, custom patterns not enabled

GHAS requires a GitHub Advanced Security license (included in GitHub Enterprise). Organizations often enable it but leave default configurations that miss critical vulnerability classes.

CodeQL Coverage and Gap Testing

CodeQL uses semantic analysis to find security vulnerabilities, but its coverage depends heavily on which query suites are enabled and which languages are configured.

Auditing CodeQL Configuration

# Check which CodeQL workflows are configured
find .github/workflows/ -name "*.yml" -exec grep -l "codeql" {} \;
cat .github/workflows/codeql.yml

# Common misconfigurations:
# 1. Only 'default' query suite (misses extended/security-experimental)
# 2. Missing language configurations
# 3. Only runs on push to main (misses PR branches)
# 4. Uses outdated CodeQL action version

# Insecure config example:
# - uses: github/codeql-action/analyze@v2
#   with:
#     queries: security-and-quality  # Good — but some use 'default' only

# Secure config:
# - uses: github/codeql-action/analyze@v3
#   with:
#     queries: security-extended,security-and-quality

CodeQL Query Coverage Gaps

CodeQL's default query suite misses several vulnerability classes. Test by introducing known-vulnerable patterns:

# Vulnerability classes frequently missed by default CodeQL queries:

# 1. Second-order SQL injection (data flows through multiple functions)
# 2. Business logic vulnerabilities (no taint tracking for logic errors)
# 3. Cryptographic misuse (weak key sizes, deprecated algorithms)
# 4. Race conditions / TOCTOU
# 5. Deserialization in less common frameworks
# 6. Template injection in custom template engines
# 7. XML external entity (XXE) in newer XML libraries
# 8. HTTP response splitting in middleware

# Test CodeQL with intentionally vulnerable code (in a test branch):
# Java SQL injection — should be detected
String query = "SELECT * FROM users WHERE id = " + request.getParameter("id");
Statement stmt = connection.createStatement();
stmt.executeQuery(query);

# If CodeQL doesn't alert on obvious patterns, the configuration is broken

SARIF Result Analysis

# Download CodeQL SARIF results via API
gh api \
  -H "Accept: application/vnd.github+json" \
  /repos/OWNER/REPO/code-scanning/analyses \
  --jq '.[0] | {id, tool: .tool.name, created_at, results_count}'

# Get specific analysis SARIF
ANALYSIS_ID=$(gh api /repos/OWNER/REPO/code-scanning/analyses --jq '.[0].id')
gh api /repos/OWNER/REPO/code-scanning/analyses/$ANALYSIS_ID \
  -H "Accept: application/sarif+json" > codeql_results.sarif

# Check alert suppression — dismissed alerts may hide real vulnerabilities
gh api /repos/OWNER/REPO/code-scanning/alerts \
  --jq '.[] | select(.state == "dismissed") | {number, rule: .rule.id, reason: .dismissed_reason, message: .message.text}' \
  -F state=dismissed -F per_page=100

# Look for alerts dismissed as "false positive" — verify each one
gh api /repos/OWNER/REPO/code-scanning/alerts \
  -F state=dismissed \
  --jq '.[] | select(.dismissed_reason == "false positive") | .rule.description'

Branch Protection Bypass

# Check if CodeQL is required to pass before merge
gh api /repos/OWNER/REPO/branches/main/protection \
  --jq '.required_status_checks.contexts'

# If CodeQL is not in required_status_checks, PRs can merge even with critical alerts
# Common gap: GHAS enabled but not enforced via branch protection rules

# Check organization-level default setup
gh api /orgs/ORG/code-security-configurations \
  --jq '.[] | {name, target_type, code_scanning_default_setup}'

Secret Scanning Bypass Techniques

GitHub's secret scanning uses pattern matching against known secret formats from 100+ providers. Secrets in custom formats, encoded strings, or split across lines evade detection.

What Secret Scanning Detects

# Secret scanning detects known provider patterns like:
# AWS: AKIA[0-9A-Z]{16}
# GitHub PAT: ghp_[0-9a-zA-Z]{36}
# Stripe: sk_live_[0-9a-zA-Z]{24}
# Google API: AIza[0-9A-Za-z-_]{35}

# Check which patterns are enabled for your org
gh api /orgs/ORG/secret-scanning/alerts \
  --jq '.[] | {secret_type, created_at, state}' | head -20

# View all supported pattern types
gh api /meta --jq '.secret_scanning_patterns[].type' 2>/dev/null || \
  curl -s https://raw.githubusercontent.com/github/advisory-database/main/secret-scanning-patterns.json

Secret Scanning Bypass Patterns

# 1. Custom/proprietary format secrets — not matched by GitHub patterns
# Internal API keys with non-standard formats bypass detection entirely

# 2. Base64 encoding
import base64
secret = "AKIAIOSFODNN7EXAMPLE"
encoded = base64.b64encode(secret.encode()).decode()
# "QUTJQSU9TRk9ETk43RVhBTVBMRQ==" — not detected

# 3. Environment variable indirection (secret never in source)
# api_key = os.environ.get('AWS_ACCESS_KEY')
# Bypasses scanning — but check .env files that might be committed

# 4. Split secrets across variables
key_part1 = "AKIAIOSFODNN"
key_part2 = "7EXAMPLE"
api_key = key_part1 + key_part2

# 5. Secrets in binary files, images, or compiled artifacts

# 6. Hex encoding
secret_hex = "414b4941494f53464f444e4e37455841..."

# Test your own custom patterns
gh api /repos/OWNER/REPO/secret-scanning/alerts \
  --jq '.[] | select(.secret_type == "custom") | {secret_type_display_name, state}'

Push Protection Bypass

# Push protection blocks commits containing detected secrets
# But it can be bypassed if the committer is an org admin or has bypass permissions

# Check push protection bypass log
gh api /orgs/ORG/secret-scanning/push-protection-bypasses \
  --jq '.[] | {actor: .actor.login, reason, created_at, secret_type}' 2>/dev/null

# Common bypass reasons: "false positive", "used in tests", "will fix later"
# Audit bypasses weekly — each one is a potential live credential

# Check if push protection is enabled organization-wide
gh api /orgs/ORG \
  --jq '.secret_scanning_push_protection_enabled_for_new_repositories'

# Check repository-level push protection
gh api /repos/OWNER/REPO \
  --jq '.security_and_analysis.secret_scanning_push_protection.status'

git-secrets and truffleHog Complementary Testing

# Run truffleHog to find secrets git history (catches what GitHub missed)
docker run --rm -v "$(pwd):/pwd" trufflesecurity/trufflehog:latest \
  git file:///pwd --since-commit HEAD~100 --only-verified

# Scan entire git history for any high-entropy strings
git log --all --full-history -p | \
  grep -E "[A-Za-z0-9+/]{40,}={0,2}" | \
  grep -v "^commit\|^Author\|^Date\|^---\|^+++\|index\|Binary" | \
  head -50

# Check .env files ever committed to the repo
git log --all --full-history -- ".env" "**/.env" "*.env"
git show HEAD:.env 2>/dev/null || git log --all -p -- .env | head -30

Dependabot Misconfiguration Exploitation

Dependabot creates PRs to update vulnerable dependencies. Misconfigurations around auto-merge create supply chain risk.

Dependabot Auto-Merge Abuse

# Check if Dependabot PRs auto-merge
cat .github/workflows/dependabot-auto-merge.yml 2>/dev/null

# Common vulnerable pattern:
# on:
#   pull_request_target:  # Dangerous — runs in privileged context
# jobs:
#   auto-merge:
#     if: github.actor == 'dependabot[bot]'
#     steps:
#       - uses: actions/github-script@v7
#         with:
#           github-token: ${{ secrets.GITHUB_TOKEN }}
#           script: github.rest.pulls.merge(...)

# pull_request_target fires with write permissions to the base repo
# If a malicious package with the same name as a Dependabot update exists,
# it could be introduced via the auto-merge path

# Safer pattern: require review before auto-merge, use pull_request (not pull_request_target)

Dependabot Configuration Audit

# Review dependabot.yml for security issues
cat .github/dependabot.yml

# Check update interval — too infrequent means long exposure window
# Bad: schedule: interval: monthly
# Good: schedule: interval: weekly (or daily for production deps)

# Check if all package ecosystems are covered
# Common gaps: missing pip/npm/maven ecosystems
# Missing: docker, terraform, github-actions

# Full secure example:
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

Transitive Dependency Gaps

# Dependabot alerts on direct dependencies — transitive deps may be missed
# Use additional tools to catch transitive vulnerabilities

# npm audit for full dependency tree
npm audit --audit-level=high
npm audit --json | jq '.vulnerabilities | to_entries[] | select(.value.severity == "critical")'

# Python: check transitive vulnerabilities
pip-audit --desc
safety check --full-report

# Java: OWASP Dependency Check (catches transitive)
mvn org.owasp:dependency-check-maven:check -DfailBuildOnCVSS=7

# Check if Dependabot alerts are being dismissed without fixing
gh api /repos/OWNER/REPO/dependabot/alerts \
  -F state=dismissed \
  --jq '.[] | {number, package: .dependency.package.name, reason: .dismissed_reason}'

Actions Workflow Permission Auditing

GitHub Actions workflows run with GITHUB_TOKEN permissions that, if misconfigured, allow privilege escalation, secret exfiltration, and supply chain attacks.

Overprivileged Workflows

# Audit all workflows for permission levels
for f in .github/workflows/*.yml; do
  echo "=== $f ==="
  grep -A5 "permissions:" $f 2>/dev/null || echo "No explicit permissions (inherits org default)"
done

# Dangerous patterns:
# permissions: write-all
# permissions:
#   contents: write      # Can push to branches
#   pull-requests: write # Can approve PRs
#   id-token: write      # Can obtain OIDC tokens for cloud auth

# Best practice: minimum required permissions per job
# permissions:
#   contents: read  # Read-only

pull_request_target Exploitation

# pull_request_target is the most dangerous trigger
# It runs workflow from BASE branch (with write access) even for fork PRs
# A fork PR can inject malicious code into the checkout path

# Vulnerable pattern:
on:
  pull_request_target:
    types: [opened, synchronize]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # DANGEROUS — checks out fork code
      - run: npm test  # Runs attacker's code with write permissions

# To test: submit a PR from a fork with malicious package.json test script
# The workflow checks out fork code and runs it with GITHUB_TOKEN write access

# Detection: find all pull_request_target workflows with explicit checkout
grep -rn "pull_request_target" .github/workflows/ -l | \
  xargs grep -l "pull_request.head.sha\|pull_request.head.ref"

Secret Exfiltration from Workflows

# Workflows that accept external input can exfiltrate secrets
# workflow_dispatch with unvalidated inputs is a common vector

# Vulnerable: user controls the 'environment' input
on:
  workflow_dispatch:
    inputs:
      environment:
        type: string
jobs:
  deploy:
    environment: ${{ inputs.environment }}  # Attacker can target any environment
    steps:
      - run: deploy.sh
        env:
          API_KEY: ${{ secrets.PROD_API_KEY }}  # Exposed to attacker-controlled env

# Find all workflows with workflow_dispatch and environment selection
grep -rn "workflow_dispatch" .github/workflows/ -l | \
  xargs grep -l "environment:"

Third-Party Action Security

# Actions referenced by branch/tag are supply chain risks
# Attacker who compromises action repo can inject malicious code

# Bad: uses: some-org/some-action@v1  (tag can be moved)
# Bad: uses: some-org/some-action@main  (branch can change)
# Good: uses: some-org/some-action@abc1234  (immutable commit SHA)

# Audit for non-SHA action references
grep -rn "uses:" .github/workflows/ | grep -v "@[a-f0-9]\{40\}" | grep -v "# pinned"

# Check for actions from unverified creators
grep -rn "uses:" .github/workflows/ | \
  grep -v "actions/\|github/\|hashicorp/\|aws-actions/\|docker/" | \
  grep -v "# trusted"

SARIF Upload Abuse

SARIF (Static Analysis Results Interchange Format) files can be uploaded to GitHub to create code scanning alerts. This feature can be abused to suppress legitimate alerts or flood the alert feed.

# Uploading a SARIF file with zero results suppresses CodeQL alerts for a run
# If an attacker gains write access to CI, they can replace the SARIF output

# Example: malicious SARIF that marks all alerts as suppressed
{
  "version": "2.1.0",
  "runs": [{
    "tool": {"driver": {"name": "CodeQL", "version": "2.14.0"}},
    "results": []  // Empty — claims no vulnerabilities found
  }]
}

# Check SARIF upload history via API
gh api /repos/OWNER/REPO/code-scanning/analyses \
  --jq '.[] | {id, tool: .tool.name, created_at, results_count, url: .url}' | head -20

# Verify SARIF was uploaded from trusted source
gh api /repos/OWNER/REPO/code-scanning/analyses \
  --jq '.[] | select(.tool.name == "CodeQL") | {created_at, results_count}' | head -10

# Flag: result count drops to 0 unexpectedly

Organization-Level Security Policy Gaps

# Check organization security settings
gh api /orgs/ORG \
  --jq '{
    "2fa_required": .two_factor_requirement_enabled,
    "members_can_create_public_repos": .members_can_create_public_repositories,
    "default_repo_permission": .default_repository_permission,
    "secret_scanning": .secret_scanning_enabled_for_new_repositories,
    "push_protection": .secret_scanning_push_protection_enabled_for_new_repositories
  }'

# Check which repositories DON'T have GHAS enabled
gh api /orgs/ORG/repos --paginate \
  --jq '.[] | select(.security_and_analysis.advanced_security.status != "enabled") | .full_name'

# Find repositories where branch protection doesn't require code scanning
gh api /orgs/ORG/repos --paginate --jq '.[].name' | while read repo; do
  result=$(gh api /repos/ORG/$repo/branches/main/protection 2>/dev/null | \
    jq -r '.required_status_checks.contexts[]' 2>/dev/null | grep -c "CodeQL" || echo 0)
  if [ "$result" -eq 0 ]; then
    echo "Missing CodeQL requirement: $repo"
  fi
done

GHAS Hardening Recommendations

CodeQL Hardening

# Use security-extended query suite and pin action version
- name: Initialize CodeQL
  uses: github/codeql-action/init@v3  # Pin to latest major
  with:
    languages: javascript, python, java
    queries: security-extended  # More queries than default

# Run on all PRs AND scheduled (for new CVEs against unchanged code)
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'  # Weekly full scan

Secret Scanning Custom Patterns

# Add custom patterns for proprietary secret formats
# GitHub organization settings → Security → Secret scanning → Custom patterns

# Example: internal API key format
# Pattern: IRON_[A-Z0-9]{32}
# This catches internal Ironimo API keys that GitHub doesn't know about

# Enable all available partner patterns
# Settings → Security → Secret scanning → Partner patterns → Enable all
GHAS Security Checklist: Enable security-extended CodeQL queries; require CodeQL to pass in branch protection; enable push protection for secret scanning; add custom patterns for internal secrets; pin all third-party Actions to commit SHAs; avoid pull_request_target with fork checkouts; set minimum permissions on all workflows.
GHAS Security TestTool/MethodPriority
CodeQL query suite auditWorkflow file reviewHigh
Secret scanning coverage gapCustom pattern testingHigh
Push protection bypass log reviewGitHub APIHigh
Dependabot auto-merge auditWorkflow reviewHigh
pull_request_target detectiongrep + manual reviewCritical
Third-party action SHA pinninggrep workflowsHigh
Workflow permissions auditgrep + GitHub APIHigh
SARIF upload integrityGitHub API historyMedium
Branch protection enforcementGitHub APIHigh

Automate GHAS Security Auditing

Ironimo continuously audits your GitHub Actions workflows, CodeQL configuration gaps, and secret scanning coverage to catch what GHAS misses.

Start free scan