DEV Community

Patience Mpofu
Patience Mpofu

Posted on

Why I Suppressed 30% of Snyk's Recommendations

188 vulnerabilities. That's what Snyk found in MFlix.

I fixed 132 of them. I suppressed 56.

If you're reading that and thinking "shouldn't you fix everything?" — that's a reasonable instinct and also the wrong mental model for how AppSec actually works in practice. This article is about why.

Not why I was lazy. Not why I cut corners. But why a security engineer who fixes every finding without thinking is doing something closer to compliance theatre than security work — and why documented, reasoned suppression decisions are a legitimate and necessary part of a mature security programme.


The Core Principle: Risk Acceptance Is Not Risk Ignorance

Every security programme operates with finite resources against an infinite potential attack surface. The question is never "have we eliminated all risk?" — that's impossible. The question is "have we reduced risk to an acceptable level, and do we have documented, defensible reasons for the risks we've chosen to accept?"

Suppressing a Snyk finding without a documented reason is risk ignorance. You've decided not to fix something and you haven't recorded why.

Suppressing a Snyk finding with a clear, reasoned justification is risk acceptance. You've evaluated the finding, understood the attack scenario, assessed the likelihood and impact in your specific context, and made a deliberate decision that the residual risk is acceptable given the cost of remediation.

The difference matters when something goes wrong — and in security, something always eventually goes wrong. "We assessed this finding and accepted the risk because X, Y, Z" is a defensible position. "We didn't get around to it" is not.

With that principle established, here's exactly how I made suppression decisions on MFlix.


The Decision Framework

Every finding went through four questions in order:

1. Is the vulnerable code path reachable in this application?
A vulnerability in a library function that MFlix never calls has zero exploitability regardless of its CVSS score. Unreachable code paths are candidates for suppression.

2. What does the exploit maturity look like?
Snyk's exploit maturity classification — mature exploit, proof of concept, no known exploit — is the most important signal after reachability. A vulnerability with working public exploit code demands different urgency than one that's theoretically exploitable but requires novel research to weaponise.

3. What are the compensating controls?
Is there something else between the attacker and the vulnerable component that reduces the practical risk? Network-level access controls, WAF rules, authentication requirements, rate limiting — these don't eliminate vulnerabilities but they change the realistic attack surface.

4. What does remediation cost versus what does it buy?
Some fixes are a one-line version bump. Others require API migrations across dozens of files, introduce breaking changes, or require testing that takes days. The remediation cost has to be weighed against the risk reduction.

A finding that scores poorly on all four — reachable, mature exploit, no compensating controls, cheap to fix — gets fixed immediately. A finding that scores well on all four — unreachable, no known exploit, multiple compensating controls, expensive to fix — is a strong suppression candidate.

Most findings fall somewhere in between, which is where judgment comes in.


The Findings I Suppressed and Why

Category 1: Unreachable Code Paths (23 findings)

The largest suppression category. These are vulnerabilities in library functions that MFlix imports but never calls.

The clearest example is a cluster of findings in jackson-databind around its polymorphic deserialization feature. The vulnerability is real — if an application uses Jackson's default typing or @JsonTypeInfo with As.PROPERTY, an attacker can instantiate arbitrary classes. The CVSS is 9.2.

MFlix doesn't use polymorphic deserialization. The application deserializes simple flat document structures from MongoDB — movie records, user objects, comments. There's no @JsonTypeInfo annotation anywhere in the codebase, no default typing configured on the ObjectMapper, and no endpoint that accepts the kind of nested polymorphic JSON the attack requires.

I verified this by searching the codebase for every Jackson configuration point and every deserialization call:

grep -r "JsonTypeInfo\|enableDefaultTyping\|activateDefaultTyping" src/
# No results

grep -r "ObjectMapper\|readValue" src/
# 3 results — all simple flat document deserialization
Enter fullscreen mode Exit fullscreen mode

Zero results for the dangerous configuration. The attack path doesn't exist in this application.

Suppression entry:

Finding: jackson-databind Deserialization of Untrusted Data (CWE-502, CVSS 9.2)
Decision: SUPPRESS
Reason: MFlix does not use polymorphic deserialization. No @JsonTypeInfo annotations,
no default typing configuration. All Jackson usage is flat document deserialization
with no user-controlled type information. Verified by codebase search 2024-11-15.
Review date: 2025-05-15
Enter fullscreen mode Exit fullscreen mode

23 findings fell into this category — all vulnerabilities in Jackson, Spring's expression language engine, and Tomcat's JSP compiler that require application-level configurations or usage patterns that don't exist in MFlix.

Category 2: No Known Exploit (18 findings)

Snyk's exploit maturity filter showed 137 findings with "no known exploit." After removing those already resolved by the BOM upgrade and those in category 1, 18 remained as candidates for suppression on exploit maturity grounds alone.

These are real vulnerabilities in the CVE database with CVSS scores and CWE classifications. They're also vulnerabilities where no researcher has published working exploit code, no proof-of-concept exists, and exploitation would require novel security research to achieve.

The practical reality: the barrier to exploitation for these findings is "someone needs to figure out how to exploit this first." For a non-production portfolio project with no external attack surface, that barrier is sufficient to justify deferral.

I want to be clear about the difference between this decision for MFlix and this decision for a production system. For a real application with real users and real data, "no known exploit today" means "no known exploit yet" — and the calculation shifts significantly. For MFlix, which has never been deployed and has no external attack surface, it's a reasonable acceptance.

Suppression entry:

Finding: [18 findings, various packages]
Decision: SUPPRESS — DEFERRED
Reason: No known exploit in Snyk database as of scan date. No proof-of-concept
available. MFlix is a non-production portfolio project with no external attack
surface. Risk accepted pending any change in exploit maturity or deployment context.
Review date: quarterly or if exploit maturity changes.
Enter fullscreen mode Exit fullscreen mode

Category 3: Non-Production Context (9 findings)

Nine findings are in test-scoped dependencies — libraries that are only used during test execution and never included in the production artifact.

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.1.0</version>
    <scope>test</scope>
</dependency>
Enter fullscreen mode Exit fullscreen mode

JUnit 5.1.0 has several findings. None of them matter for production security because JUnit never ships in the application JAR. A vulnerability in the test framework cannot be exploited by an attacker targeting the running application — it's not there.

This is a common source of inflated finding counts in SCA tools. They scan the full dependency tree including test scope, which is appropriate for completeness but requires scope-aware triage.

Suppression entry:

Finding: junit-jupiter-api@5.1.0 [various findings]
Decision: SUPPRESS
Reason: Test-scoped dependency. Not included in production artifact.
No exploitability from external attack surface.
Enter fullscreen mode Exit fullscreen mode

Category 4: Accepted Residual Risk Post-Remediation (6 findings)

Six findings remain after I applied every fix that was reasonably achievable without a full rewrite of the application's data access layer.

The specific case: mongodb-driver-sync@3.9.14.x is a major version upgrade with significant breaking API changes. The 3.x and 4.x MongoDB Java driver APIs are substantially different — connection management, session handling, and the reactive streams API all changed. Migrating would require rewriting significant portions of the DAO layer.

The vulnerability itself — a Man-in-the-Middle risk in the TLS connection handling, CWE-300, CVSS 6.4 — is real but has a compensating control: the MongoDB Atlas connection string already enforces TLS (mongodb+srv:// scheme requires TLS). The MitM risk is partially mitigated by the network-level TLS enforcement even with the driver-level validation weakness.

I upgraded to the latest 3.x patch (3.12.x) which addresses several other findings, documented the residual risk from the 4.x migration, and flagged it as a tracked backlog item rather than a suppression.


What I Fixed That I Could Have Suppressed

This is the part of this article that's most important for understanding how AppSec judgment actually works.

Several findings I fixed could have been legitimately suppressed under the framework above. I fixed them anyway. Here's why.

The jackson-databind findings I fixed: Alongside the suppressed polymorphic deserialization findings, there were jackson-databind findings in the JSON parsing path for user-submitted data — the comment posting and user registration endpoints. These are reachable from the application's attack surface. Even without default typing, certain jackson-databind parsing vulnerabilities can be triggered by malformed JSON input. These got fixed.

The Tomcat findings I fixed: The Insecure Defaults finding in tomcat-embed-core@8.5.31 (CVSS 9.8) could have been argued as "we just won't enable AJP" and suppressed. I fixed it instead because CVSS 9.8 with a working proof of concept is a line I'm not comfortable crossing even with a compensating control argument. The fix was a version bump. The risk of not fixing it wasn't worth the two minutes it would have taken to suppress it.

The Spring Security authorization bypass: This one had a mature exploit classification. Regardless of how difficult the exploitation path was to follow in MFlix specifically, "Spring Security authorization bypass with a mature exploit" is not a finding I was going to suppress. Authentication and authorization are the core security controls in this application. I fixed it.

The principle: suppression requires genuine cost-benefit analysis. When the fix is cheap and the risk is non-trivial, fixing is always the right answer. Suppression is for when the fix is expensive or the risk is genuinely minimal — not for when fixing is inconvenient.


Documenting Suppressions in Practice

Snyk supports inline suppression via code comments and also through its dashboard where you can mark findings as "ignored" with a reason and expiry date.

For MFlix I used Snyk's ignore functionality in .snyk file format:

# .snyk
version: v1.25.0
ignore:
  SNYK-JAVA-COMFASTERXMLJACKSONCORE-*:
    - '*':
        reason: >
          Polymorphic deserialization not used in MFlix. No @JsonTypeInfo
          annotations. All Jackson usage is flat document deserialization.
        expires: '2025-05-15T00:00:00.000Z'
        created: '2024-11-15T00:00:00.000Z'
  SNYK-JAVA-ORGJUNITJUPITER-*:
    - '*':
        reason: Test-scoped dependency. Not in production artifact.
        expires: '2025-11-15T00:00:00.000Z'
        created: '2024-11-15T00:00:00.000Z'
Enter fullscreen mode Exit fullscreen mode

Two things about this format worth noting:

Expiry dates are mandatory in my process. Every suppression gets a review date. Security posture changes — exploit maturity changes, deployment context changes, the application gets extended with new functionality that makes previously unreachable code paths reachable. A suppression without an expiry date is a suppression that gets forgotten.

Reasons are complete sentences. Not "not applicable" or "false positive." A complete sentence explaining exactly why the finding doesn't apply or why the risk is accepted. Future-you reading this six months later needs to understand the reasoning, not just the conclusion.


The Numbers

Category Count % of Total
Fixed by BOM upgrade 89 47%
Fixed by targeted remediation 43 23%
Suppressed — unreachable code path 23 12%
Suppressed — no known exploit 18 10%
Suppressed — test scope 9 5%
Tracked — accepted residual risk 6 3%
Total 188 100%

The BOM upgrade doing 47% of the work in one change is the most important number in that table. It's the strongest argument for keeping your framework versions current — not just for features but because the framework's own dependency management does an enormous amount of security maintenance work for you automatically.


What This Looks Like in a Real Organisation

In a production AppSec programme, this decision process doesn't happen in isolation. It involves:

  • A risk register where accepted risks are formally documented and reviewed on a defined cadence
  • Security team sign-off on suppressions above a certain severity threshold
  • Engineering sign-off on the technical reasoning for unreachability claims
  • Audit trail that shows who made each decision and when

For MFlix — a portfolio project — I've approximated this with the .snyk file, this article, and the commit history. For a real team, the process would be more formal but the underlying reasoning is identical.

The skill being demonstrated here isn't "I know how to run Snyk." It's "I can evaluate a finding, reason about its applicability to a specific system, make a defensible risk decision, and document it in a way that survives scrutiny."

That's application security engineering. The tool is just the starting point.


The .snyk file with all suppression entries and the full remediated pom.xml are in the repository at github.com/pgmpofu/mflix.

Next up: the remediation work itself — the version bumps that were easy, the ones that introduced breaking changes, and the MongoDB driver upgrade that I decided wasn't worth the cost.

Top comments (0)