ARTICLE
Reducing Classes of Vulnerability The Rules That Fail Our Build
Published 1 September 2026 · By P Larner
Secure by Design#memory safety#SQL injection#Secure by Design#XSS#static analysis
Reducing Classes of Vulnerability: The Rules That Fail Our Build
Illustration for: Reducing Classes of Vulnerability: The Rules That Fail Our Build
The third goal of CISA’s Secure by Design pledge asks for actions “enabling a significant measurable reduction in the prevalence of one or more vulnerability classes across the manufacturer’s products”. The wording is doing deliberate work. Fixing bugs one at a time is not the ask. Making a category of bug impossible, or at least impossible to merge, is the ask.
We picked three classes, which are SQL injection, cross-site scripting and memory safety. This article sets out what stops each one, and then corrects three places where our own pledge page describes the enforcement more confidently than the pipeline deserves.
Memory safety comes from the runtime, with one boundary
RaptorGRC is written in C# on .NET. Memory safety is a property of the runtime rather than something we implemented, which is why CISA lists writing new products in memory-safe languages as an approach in its own right. There are no unsafe blocks in the product source.
There is one boundary worth naming rather than glossing over. Active Directory and LDAP authentication use the .NET directory services library, which calls into the native OpenLDAP client library inside the container. That is native code we did not write and cannot make memory safe. It is a small and well-known surface, it is only in the execution path if you configure LDAP authentication, and it is the single place where “memory safe throughout” would be an overstatement.
SQL injection is closed by the data layer and then by the compiler
Data access is Entity Framework Core throughout. Queries are composed in LINQ and values reach the database as parameters.
Two places in the product execute raw SQL. Both sit inside data-migration seeders that rename stored values, and both pass their values through parameter placeholders rather than building a string. There is no string-concatenated SQL anywhere in the product source, and none at all on this site.
That is a convention, and conventions decay as teams change, so it is backed by the compiler. The .NET injection analysers, CA3001 through CA3012, are switched on in the solution’s editor configuration, and the build treats warnings as errors, so a finding from any of them breaks the build. Those rules cover SQL, LDAP, XPath, XML and process-argument injection. A developer who introduces a concatenated query does not receive a warning they can scroll past. They receive a build that does not compile.
That combination is also why the analysers are load-bearing rather than decorative. SonarQube Community Build has no taint analysis, so without these rules and the Semgrep job described below, nothing in the pipeline would be following untrusted values from an entry point to a sink.
This is the part of the goal I am most comfortable claiming, because it does not depend on anyone remembering anything.
Cross-site scripting is handled by the framework and by an allow list
Blazor encodes rendered values by default, so the ordinary path is safe without a developer thinking about it. The interesting cases are the few places where we deliberately render stored HTML.
In the product that is governance documents and investigation narratives. On this site it is guide and blog bodies, which are authored in Word and converted to HTML at upload. Both paths run through an allow-list sanitiser with a fixed set of structural and formatting tags, a fixed set of attributes, and links restricted to http, https and mailto. No styles, no scripts, no frames.
On this site the sanitiser runs twice, once when the document is uploaded and again every time a body is rendered. The second pass is redundant if the first one worked, which is the point of having it. It also strips remote image sources outright, so a post body can carry inline images only and can never leak a reader’s IP address to a third-party host.
Two related classes are closed alongside it. Antiforgery protection is on globally in the product, with two endpoints opting out for reasons written into the code, those being the SAML assertion consumer, because an external identity provider cannot carry our token, and the incident ingest API, which authenticates with an API key instead. Return URLs pass through a helper that rejects absolute, protocol-relative and backslash-relative values, which closes the open-redirect class.
What the pipeline gates
The product build pipeline runs the following at the release stage.
- Semgrep taint and pattern analysis over two rulesets vendored into the repository rather than pulled from a registry at scan time, so the rules are pinned and the job works without network access, configured so that any error or warning finding fails it.
- Trivy against the filesystem for secrets and misconfiguration, failing on critical and high.
- Trivy against the built image, failing on critical and high where a fix exists.
- A NuGet audit for vulnerable direct and transitive packages, failing on high and critical advisories.
- A SonarQube quality gate that the pipeline waits for rather than firing and forgetting.
The scan policy is committed alongside the pipeline, so the rule is public rather than folklore. A release fails on any critical or high finding that has a fix available.
Two gates at two speeds, and why that is deliberate
It matters where in the pipeline each of those checks sits, because a check that runs only at release time is not protecting a developer from themselves on a Tuesday.
The injection analysers gate every merge request
The Roslyn rules run on every build, which means every merge request. That is the continuous gate, and it is the one that shapes how code gets written, because feedback arrives while the author still has the change in their head.
The deeper scans gate every release
The Semgrep job and the container scan carry release rules. They run on a manually triggered pipeline, a version tag, or an explicit release trigger, and the pipeline file says so in its own comment, which is that this is a release pipeline rather than per-commit CI and the quality job continues to serve merge requests.
That split is a deliberate design rather than a shortfall. Semgrep’s taint analysis over two vendored rulesets is slow and the container scan needs a built image, so running both on every push would push merge feedback out to the point where people stop reading it. Running them before anything reaches a customer catches the same findings at the last useful moment.
It does mean a taint-analysis finding can live on a branch for a while before anything says so. I would rather that than a fifteen-minute merge request pipeline that developers learn to ignore.
Suppressions have to be argued for
Semgrep runs with any error or warning failing the job. A triaged false positive is suppressed with an inline comment naming the specific rule, and the convention is that a justification is written beside it in the code.
That is worth more than it sounds. A suppression with a reason next to it is reviewable in the diff and readable by whoever inherits the file. A blanket exclusion in a configuration file is neither.
Two qualifications on the gates
The quality gate has an override
The SonarQube gate can be overridden by a flag set on the pipeline form. It is off by default, it has to be chosen deliberately, and when it is used the pipeline prints a line recording in plain words that a release went out with a failing gate.
It exists because a blocked release caused by a false positive is itself a risk, and a team that cannot ship a security fix because of a code-smell rule will eventually route around the pipeline entirely. But an escape hatch is an escape hatch, and describing the gate as absolute would be false. What makes it acceptable is that using it leaves a permanent, unmissable record.
The scanners only fail on findings with a fix
A critical vulnerability in a dependency with no upstream patch does not stop a release, because stopping would achieve nothing except keeping our own fixes away from customers. It is recorded in the scan report that ships with the release, which is the subject of the sixth article in this series.
That is a deliberate policy and it is published in the repository as a scan policy document, but it means a reader who saw only “the build fails on critical findings” would form the wrong picture.
What is not covered
Two classes are not addressed by anything above.
There is no dedicated guard against server-side request forgery on outbound requests. That matters wherever a deployment is configured to reach an external vulnerability scanner, an identity provider or an optional AI endpoint, because those are exactly the features that take a customer-supplied URL and fetch it. The open-redirect helper does not help here, and I could find no private-address blocking on outbound calls.
We have also not evidenced any content inspection or malware scanning on uploaded attachments beyond controlled storage paths. A GRC platform is a document store by nature, so this one deserves attention.
Why this goal stays open
CISA suggests measuring this goal through root-cause analysis of CVEs over time, watching a class shrink in the record.
RaptorGRC has no CVEs, so there is no trend line to show. The reduction here is structural rather than demonstrated, and structural claims age badly if nobody keeps testing them.
That is why the pledge page treats this goal as permanently in progress rather than shipped. The commitment that means something is not the list of analysers above. It is that each release cycle picks the next class worth removing. Server-side request forgery is the obvious candidate for the next one, and if this article still says that in a year, it will be a fair thing to hold us to.
The full tracker is at /secure-by-design. What does and does not leave your network is set out at /transparency.
Run your GRC programme in your own network.
RaptorGRC Community Edition is free: every module, offline licence activation, nothing phones home.
Register / Download Contact us