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.
GitHub Advanced Security combines three security layers, each with distinct attack surfaces:
| GHAS Feature | Function | Primary Attack Surface |
|---|---|---|
| CodeQL | SAST — finds code vulnerabilities via semantic analysis | Coverage gaps, query exclusions, language support limits |
| Secret Scanning | Detects committed secrets matching provider patterns | Custom format secrets, encoded/split secrets, bypass techniques |
| Dependabot Alerts | Identifies vulnerable dependencies | Transitive deps, version pinning, auto-merge abuse |
| Dependabot Security Updates | Auto-creates PRs to fix vulnerable deps | Auto-merge misconfig, dependency confusion |
| Push Protection | Blocks commits with detected secrets | Bypass 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 uses semantic analysis to find security vulnerabilities, but its coverage depends heavily on which query suites are enabled and which languages are configured.
# 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'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
# 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'
# 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}'
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.
# 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
# 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 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'
# 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 creates PRs to update vulnerable dependencies. Misconfigurations around auto-merge create supply chain risk.
# 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)
# 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"
# 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}'
GitHub Actions workflows run with GITHUB_TOKEN permissions that, if misconfigured, allow privilege escalation, secret exfiltration, and supply chain attacks.
# 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 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"
# 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:"
# 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 (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
# 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
# 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
# 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
pull_request_target with fork checkouts; set minimum permissions on all workflows.
| GHAS Security Test | Tool/Method | Priority |
|---|---|---|
| CodeQL query suite audit | Workflow file review | High |
| Secret scanning coverage gap | Custom pattern testing | High |
| Push protection bypass log review | GitHub API | High |
| Dependabot auto-merge audit | Workflow review | High |
| pull_request_target detection | grep + manual review | Critical |
| Third-party action SHA pinning | grep workflows | High |
| Workflow permissions audit | grep + GitHub API | High |
| SARIF upload integrity | GitHub API history | Medium |
| Branch protection enforcement | GitHub API | High |
Ironimo continuously audits your GitHub Actions workflows, CodeQL configuration gaps, and secret scanning coverage to catch what GHAS misses.
Start free scan