Safeguard
Best Practices

AppSec anti-patterns to eliminate

23.8M secrets leaked on public GitHub in 2024 alone. Here are the AppSec anti-patterns behind numbers like that — and the concrete practices that replace them.

Safeguard Research Team
Research
6 min read

GitGuardian's State of Secrets Sprawl 2025 report counted 23.8 million new hardcoded secrets leaked on public GitHub commits in 2024, a 25% increase over the year before, and found that 70% of secrets leaked back in 2022 were still valid and exploitable when researchers checked again years later. That single statistic captures most of what is wrong with how software gets built today: a credential gets typed into a config file during a debugging session, a commit goes out, and nobody circles back to rotate it — sometimes for years. Hardcoded secrets are only one entry on a longer list of AppSec anti-patterns that repeat across nearly every codebase, alongside missing SAST gates in CI, unpinned or confused dependency resolution, and policies that exist on paper but block nothing in practice. None of these are exotic failures; they are default behaviors that emerge when security is treated as a review step instead of a build-time property. This post catalogues the anti-patterns that show up most often in real incident postmortems and audits, and for each one lays out the concrete, buildable practice that replaces it — from pre-push secret scanning to reachability-aware dependency gates.

Why do hardcoded secrets keep leaking at scale?

Hardcoded secrets keep leaking because committing a credential is the path of least resistance during development, and nothing in the default git workflow stops it. GitGuardian's 2025 report found generic secrets — passwords, private keys, and connection strings not tied to a specific issuer's format — made up 58% of all leaks, precisely the category that pattern-only scanners struggle to catch reliably. The report also flagged that repositories using GitHub Copilot showed a 6.4% secret-leakage rate, suggesting AI-assisted coding is reproducing hardcoded credentials from training patterns rather than eliminating them. The fix isn't a single scan before merge; it's layered detection — issuer-specific regex for known providers, generic-pattern matching, and entropy analysis for the rest — combined with live verification against the issuing service (an AWS sts:GetCallerIdentity call, a GitHub /user lookup) so a finding is confirmed live-and-exploitable, not just plausible. Safeguard's secrets scanning applies all three detection layers plus verification, and ships a pre-push git hook so a credential is caught before it ever reaches a remote — closer to the 0% leak rate the 70%-still-valid statistic shows most organizations are nowhere near.

Why is "we run a scanner" not the same as having a SAST gate?

Running a scanner and enforcing a gate are different things, and conflating them is one of the most common AppSec anti-patterns: a SAST tool produces findings in a dashboard nobody checks, while the pipeline that shipped the vulnerable code merges and deploys anyway. A real gate ties a specific, mapped finding (a CWE-89 SQL injection, a CWE-502 unsafe deserialization) to a CI/CD checkpoint that can actually block, warn, or require approval — not just log. Safeguard's application security testing traces untrusted input from source to sink and attaches a CWE/OWASP mapping and a dataflow trace to each finding, and its gate framework supports Block, Warn, Notify, and Require Approval actions wired directly into pipelines via a GitHub Action. It's worth being precise about maturity here: Safeguard's SAST/DAST is a rolling platform, with phase-one language coverage limited to JavaScript/TypeScript, Python, and Java — detection depth is expanding, not complete — so gates should be scoped to the languages and rule sets you've actually validated, not assumed to cover everything in your stack.

What makes a dependency "shadow," and why is it dangerous?

A shadow dependency is any package your build resolves and executes that isn't the one your team actually intended to install — and the clearest demonstration of how dangerous that gap is came from security researcher Alex Birsan's February 2021 published research, "Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies." Birsan found that internal package names used by more than 35 companies — including Apple, Microsoft, PayPal, Shopify, Netflix, Tesla, Uber, and Yelp — had no corresponding entry on public registries like npm and PyPI. He published his own packages under those exact names with higher version numbers, and internal build systems, following default resolution rules that prefer the highest available version regardless of registry, pulled and executed his code automatically inside dozens of corporate environments. He was paid more than $130,000 in bug bounties for the disclosure. The defense is resolution hygiene enforced at the point packages are fetched, not after the fact: scoped/private-registry-first resolution, lockfiles pinned to exact versions and hashes, and an install-time check that flags typosquats and namespace-confusable names before a package lands in your tree — which is the model behind Safeguard's Package Firewall, an install-time proxy in front of npm and pip that evaluates every fetch, transitive dependencies included, before it reaches disk.

Why do CVE counts from a dependency scan mislead more than they inform?

A raw dependency scan tells you every known-vulnerable package version present in your manifest, but it says nothing about whether your code ever calls the vulnerable function — and treating every CVE as equally urgent is how security teams end up with backlogs of hundreds of tickets that never get triaged. The anti-pattern is running software composition analysis (SCA) as a pass/fail gate on CVE presence alone, which produces so much noise that teams start ignoring the queue entirely, including the handful of findings that are genuinely reachable and exploitable. The fix is reachability-aware triage: cross-referencing each CVE match against a call graph built from your actual entry points, so a finding in a transitive dependency your code never imports is de-prioritized relative to one sitting directly on a path a request can reach. Safeguard's SCA engine does exactly this — resolving the full dependency graph, matching against CVE/GHSA advisories enriched with EPSS and CISA KEV context, and layering reachability so triage time goes to the packages that matter, with the standing rule that malware and secret findings are never suppressed by a reachability verdict regardless of how deep they sit.

How do you turn a written security policy into something that actually blocks bad deploys?

Most organizations have a security policy document; far fewer have one wired into a pipeline in a way that can stop a deployment. The anti-pattern is a policy that lives in a wiki and gets referenced during incident retros instead of being evaluated automatically on every build. A working policy-and-gate setup defines machine-checkable conditions — no CVSS above a threshold, no actively-exploited (high-EPSS) CVEs, required SLSA attestation level, no components past a maintenance-age cutoff — and attaches a real enforcement action to each one: block, warn, notify, or require manual approval with an expiring exception. Safeguard's policy engine supports exactly these condition types, including abandoned-package and SLSA-level checks, enforced through a CI gate action that fails a pipeline on violation rather than just recording it. The operational discipline that makes this durable is the exception workflow: every override requires a business justification, an expiration date, and an audit trail, so "temporary" exceptions don't quietly become permanent blind spots.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.