research

Putting models to the secure coding test: Security rules

October 19, 2026

Putting Models To The Secure Coding Test: Security Rules

Engineers no longer write all the code. With coding agents, people who have never coded before now are empowered to create software. And honestly, can we still say people are writing code when agents are writing it for us? This shift brings many benefits, but it also worries me from an application security perspective. How can we ensure that agents write secure code?

This led me to investigate an overarching question: How well do coding agents write secure code, and what can be done to improve it? I'll spend the next few months trying to answer this question and will share my findings in this series of blog posts. If you missed the first part of this series where I tested how plan mode compared to the default mode when writing secure code, check it out here.

My second research question, and the focus of this post, is whether running a model with pre-set security rules has a significant security effect compared to not providing any security rules. It seems like this question should have an obvious answer—“of course!”—but I want to see just how impactful good security rules are.

Methodology

I started this experiment by listing out all the security requirements that I wanted to include in the prompt. As you can imagine, this list started to get long. I also acknowledged that knowing what security guidelines to add requires a basic application security understanding. This led me to see if anyone had already laid out security rules to be used when coding with agents. That’s when I discovered Project CodeGuard, an open source security framework that provides security rules for coding agents to follow.

When using Claude Code, I downloaded the latest version of Project CodeGuard (v1.4.0) from the plugin marketplace and installed it locally so it was only in the sonnet-5-security folder that I was working in. Cursor and Codex have a different installation method: When working with these agents, I downloaded the appropriate zip file and ran the associated commands from the Project CodeGuard installation guide.

To maintain consistency, I utilized the same prompt for testing across three models: Sonnet 5, Composer 2.5, and GPT 5.5. I used Claude, Cursor, and Codex with their corresponding models. I used the same prompt as in the first experiment. Every iteration of that experiment had an insecure direct object reference (IDOR) vulnerability because the prompt did not specify who should have access to the documents when uploaded. I was curious to see if the security rules would make the agent think differently about access.

I only ran the experiment once for each model. This allowed me to go more in depth on that specific run, but I do acknowledge that because AI is nondeterministic not everyone run would respond the same way.

The prompt was:

Create an application with the following guidelines: 
Tech stack
Javascript (npm)
JWT + session cookies
PostgreSQL
Application requirements
Frontend
Backend API
Login functionality
Multiple user roles
Password reset
Forget password
Database
Search 
File upload
Comments
Profile editing
Create a GitHub workflow to deploy the app via GitHub pages
a small “document portal” where users log in, upload files, search documents, comment, and admins manage users. 
Do NOT reference any files outside of this folder. 

Once again, I used Supply Chain Firewall (SCFW) to help prevent the installation of malicious packages on my machine. While this did have a small impact on the security output, I felt it was a needed protection in light of many recent supply chain attacks.

I used Datadog Code Security to identify software composition analysis (SCA), static application security testing (SAST), and infrastructure-as-code (IaC) vulnerabilities, as well as any code quality suggestions. To dig deeper into security vulnerabilities, I used Claude’s code_review skill using Sonnet 5 across all iterations to audit the code.

If you'd like to review the full code for each iteration, you can find it in this repository.

Models without security rules

Since I was using the same prompt, models, and mode as the last experiment, I decided to reuse the same results. I will give a brief summary of how each model performed, but you can check out the in-depth analysis if you want to know more. I ran Sonnet 5, Composer 2.5, and GPT 5.5 in their default mode without security rules.

Across all three iterations in default mode, a recurring architectural blind spot emerged: Any authenticated session could access, retrieve, or annotate files belonging to others, a clear IDOR vulnerability. Beyond this shared weakness, the specific failure modes varied. Sonnet 5 delivered a noticeably more defensive baseline, implementing magic-byte verification, rotation of refresh tokens, and double-submit CSRF, which made its failure to scope document access an interesting oversight in an otherwise hardened application. Composer 2.5 introduced significant risk by including a hardcoded JWT_SECRET fallback, relying on client-provided MIME types, and storing sensitive reset tokens in plaintext without any rate-limiting protections. GPT 5.5 struggled with a cross-site request forgery (CSRF) vulnerability in its upload handling and shared the same MIME-type trust issues, despite maintaining secret management elsewhere.

Models using security rules

For this experiment, the Project CodeGuard rules need to be installed and enabled once for each harness; then, they continue to run in the background until specifically disabled. They are able to be installed across the entire harness or in individual repositories, depending on the use case. This makes it easy for anyone to set and forget these guardrails, hopefully reducing the burden of implementing security measures. However, security rules are only as good as their results, so let's dig into those.

Sonnet 5

As this was my first time using the codeguard-security Claude plugin, I wasn’t sure how it would look. In this case, with the plugin enabled, it provided a plan for me to accept before moving forward, even though I was in manual mode. It listed out the security posture and the auth design, as well as the repository structure (not shown). This was the only harness where this occurred.

The plan Sonnet 5 presented for approval
The plan Sonnet 5 presented for approval (click to enlarge)

This model produced the most sophisticated architecture, utilizing an npm-workspaces root to manage a decoupled backend/ and frontend/. The frontend was built using React 19 and Vite to create a single-page application (SPA). On the backend, it followed a strict model-view-controller (MVC) pattern across directories like controllers/, services/, and config/, with a dedicated Dockerfile. The authentication strategy was equally robust, pairing short-lived JSON Web Tokens (JWTs) with cryptographically secure pseudorandom number generator-based session tokens secured by HttpOnly; Secure; SameSite=Strict flags and double-submit CSRF protections.

This iteration did try to use outdated dependencies that were caught by the SCFW and properly updated. This was interesting, as the default and plan iterations of Sonnet 5 were the only previous runs that used up-to-date dependencies from the start.

Supply Chain Firewall blocking install
Supply Chain Firewall blocking install (click to enlarge)

Claude performed a verification step this time that I do not remember seeing in the past. The recap mentioned that a bug was found, specified what Claude did to fix it, and provided me with a high-level summary of what the verification tested for.

Sonnet 5's verification recap
Sonnet 5's verification recap (click to enlarge)

With these initial security planning and verification steps added, I was hopeful that the results from by code_review would show significant improvement. However, I was disappointed to see that the first result was the same IDOR vulnerability found in the default-mode and plan-mode iterations.

The code_review output for Sonnet 5 with security rules
The code_review output for Sonnet 5 with security rules (click to enlarge)
Sonnet 5 thinking through a potential vulnerability
Sonnet 5 thinking through a potential vulnerability (click to enlarge)

The results weren’t all bad, however: With Project CodeGuard rules added, Sonnet 5 produced an application with strong low-level hygiene, undermined by authorization and session gaps. Every query is parameterized and user input renders through React text nodes, which escape by default.

But three medium-severity gaps break the access model.

  1. The document-ownership IDOR vulnerability, present in all earlier iterations of this application, remains in this version.
  2. Revocation (logout, account deactivation) is only checked in the CSRF middleware, which only runs on mutating requests. A deactivated or logged-out user keeps read access for up to the access token's 15-minute lifetime.
  3. Session fingerprints, like user agent and IP, are captured on login but never compared afterward.

The remaining low-severity findings are narrower and more straightforward to fix:

  • No CSRF check on the token refresh endpoint
  • Rate limiting keyed only by IP address
  • An unused COOKIE_SIGNING_SECRET value
  • An account-enumeration oracle on registration, which returns a distinct 409 status when an email is already taken

Composer 2.5

This iteration produced a standard two-package Node.js monorepo, featuring a decoupled frontend/ and backend/. The frontend was a minimal Vite-powered SPA that stuck to native JavaScript and CSS, bypassing modern UI frameworks entirely. On the backend, logic was organized across routes/, middleware/, and services/ (for email handling), with a dedicated db/ folder managing migrations and connection pooling. The authentication strategy paired JWT bearer tokens with HttpOnly session cookies, utilizing PostgreSQL for both relational storage and full-text search capabilities. Files were handled via a local uploads/ directory. For deployment, the app used a GitHub Action to host the static frontend on GitHub Pages, while the API was hosted independently, with a docker-compose.yml provided for local database orchestration.

SCFW once again caught the use of vulnerable dependencies, and Cursor upgraded them appropriately.

Composer 2.5 upgrading dependencies after SCFW blocked the vulnerable versions
Composer 2.5 upgrading dependencies after SCFW blocked the vulnerable versions (click to enlarge)

I found Composer 2.5's security-focused implementation to be the weakest of the three, and the IDOR vulnerability continues to be a problem.

The code_review findings for Composer 2.5 with security rules
The code_review findings for Composer 2.5 with security rules (click to enlarge)

The worst finding besides the IDOR has a critical severity: The app verified JWTs by signature and expiry alone, with no check against a revocation state. To make matters worse, logging doesn’t actually log you out: Tokens stored are in localStorage and a stolen token that keeps working for up to 24 hours after the user logs out or resets their password.

In addition, both JWT_SECRET and SESSION_SECRET fall back to hardcoded values. The model wrapped them in a requireEnv helper that appears to throw an error in production when the variable is missing, but because the fallback is applied before the check, the check never fires. The server starts in any environment, including production, with the embedded secret, allowing anyone who has seen the source code to forge JWTs. The security rules prompted the model to add what looks like a safeguard, but it doesn't change the outcome.

Two more findings compound the problem.

  1. File uploads are validated by client-supplied MIME type and file extension, both attacker-controlled, instead of the file's actual contents.
  2. Any authenticated user can read or download any other user's documents.

GPT 5.5

Before running each iteration, I did confirm that CodeGuard was enabled. I found Codex provided the clearest response.

Codex confirming that CodeGuard was loaded and active
Codex confirming that CodeGuard was loaded and active (click to enlarge)

This model approached its server-side structure with a more deliberate focus on security, arranging logic within server/src/ across dedicated directories for security/, validation/, middleware/, and utils/. It also included a dedicated test/ suite alongside custom migrate.js/seed.js helper scripts. For the frontend, it favored a minimal Vite setup (limited to src/main.js and styles.css), bypassing modern UI component frameworks. From a cryptography perspective, it made stronger choices by selecting Argon2id instead of bcrypt for hashing passwords. Its authentication mechanism took a defensive stance, enforcing HttpOnly cookie-based JWTs integrated with server-managed session revocation and requiring CSRF tokens on state-modifying requests.

The code_review findings for GPT 5.5 with security rules
The code_review findings for GPT 5.5 with security rules (click to enlarge)

In a truly shocking turn of events, we finally had an iteration that did not have an IDOR vulnerability! The vulnerabilities present have more narrow scopes. However, it did find that new password-reset requests do not invalidate earlier unexpired tokens, and the reset endpoint performs expensive Argon2id hashing before checking token validity. The review also found that the the app computed an ip_hash value meant to bind a session to the IP address that created it, but never actually compared it during auth checks. A stolen cookie worked from any IP, as long as the attacker also spoofed the same User-Agent string. This iteration did set a manager role that had broad access to every user's documents and a single global rate limit shared across all auth routes. These were design choices that did not cross the vulnerability threshold.

Analysis

As with the first experiment in this series, the prompt makes as much of a difference to the security of a vibe-coded application as the tests do—in this case, the use of security rules. Two of the iterations from this second experiment contained an IDOR vulnerability, which is unsurprising, as I don’t specify in the prompt whether the documents should be available to everyone or just the user that uploads it. In a perfect world, these coding agents would recognize that they should default to a restricted access model, rather than leaving documents open to everyone.

Using the Project CodeGuard security rules got each model to fix a different subset of problems, but not the same subset, and not all of them. The GPT 5.5 implementation is the only build that closed the cross-user document IDOR that showed up everywhere in default mode. Sonnet 5 kept good injection and XSS hygiene but left the authorization layer half-finished. The Composer 2.5 implementation introduced the same persistent-token-after-logout bug and hardcoded secret fallbacks as the default iteration.

None of this is random. Every fixed and unfixed issue traces back to whether the original prompt named the constraint explicitly. "Build this securely" doesn't give a model the specific requirement "filter every document query by owner." The prompt needs to say it explicitly, or the model will decide.

I once again took a look at how each iteration went about dependency management. The Sonnet 5 and Composer 2.5, both using security rules, used semantic versioning (^) to allow automatic upgrades to accept any compatible minor version, just like the default iterations did. However, GPT 5.5 set upgrades to only use a specific version. While this can lead to more out-of-date dependencies, it greatly reduces the risk of using a new malicious version. The best scenario would be to use dependency cooldowns to provide a buffer before upgrading a dependency.

In the future, there are certain additions I would make to the security rules, the global agents.md (or in Claude’s case, claude.md), or the prompt. The first two options are preferable, as they would persist across all projects so security is always considered.

# ~/.config/agents/AGENTS.md
Always implement a dependency cooldown of 7 days unless otherwise specified. If the package manager does not support this, do not configure the dependency to automatically upgrade to the next version. 

Security rules versus no security rules

Using security rules with Sonnet 5 produced no meaningful improvement in security posture. The Sonnet 5 default-mode iteration already uses double-submit CSRF, refresh-token rotation, and session revocation on logout, but the build with security rules introduces a regression where that revocation check only runs on CSRF-protected routes, not in the general auth middleware, so a deactivated user keeps read access for the life of their access token. The unscoped document-read IDOR vulnerability is present in both iterations.

The default-mode and security rules iterations of Composer 2.5 differ mostly in framing rather than substance. The security iteration adds rate limiting to the authentication routes, which the default build lacked entirely. Both builds fall back to hardcoded JWT and session secrets; the security iteration adds an environment check that never actually fires. Both also issue JWTs that stay valid for up to 24 hours after logout or password reset, and the same document-ownership IDOR persists in both.

Of the three models, GPT 5.5 showed the clearest improvement when security rules were added. Its default implementation has a real CSRF vulnerability and trusts client-supplied MIME types for file validation. The iteration with security rules included a double-submit CSRF with Origin/Sec-Fetch-Site checks and magic-byte upload validation. GPT 5.5 is also the only model where the security build does not have the IDOR vulnerability and does not automatically upgrade dependencies, making it the one case where the security-focused pass produced a meaningfully more secure app.

Conclusion

While the GPT 5.5 iteration using security rules made a significant improvement, the security posture of the Sonnet 5 and Composer 2.5 iterations were largely unchanged. This surprised me, as I was expecting significant improvement across all models. The rules can be helpful but are not always specific enough to make a difference.

Findings

A detailed breakdown of the vulnerabilities can be found in the repository.

Metric Sonnet 5 Composer 2.5 GPT 5.5 Sonnet 5 w/ rules Composer 2.5 w/ rules GPT 5.5 w/ rules
SAST findings 1 17 1 0 4 0
Direct dependency quantity 13 12 17 19 14 16
Transitive dependency quantity 110 139 147 139 118 156

Did you find this article helpful?

Subscribe to the Datadog Security Digest

Get the latest insights from the cloud security community and Security Labs posts, delivered to your inbox monthly. No spam.

Related Content