AI coding tools can generate application code, write tests, fix bugs, and suggest improvements. They can also introduce security problems that are not immediately visible during a normal code review.
A generated method may compile successfully and pass its unit tests while still allowing SQL injection, exposing sensitive information, or bypassing authorization checks. The code may look clean and follow the existing project structure, but that does not mean it is safe for production.
This creates a responsibility for development teams: AI-generated code must go through the same security review process as code written by a developer.
The review should not stop at checking whether the code works. It should examine how the code handles user input, authentication, authorization, database access, external services, secrets, and errors.
In this article, we will build a practical security review process for AI-generated C# code and show how to detect common problems before merging a pull request.
Why AI-Generated Code Needs Security Review
AI coding tools generate code based on the context, instructions, and examples available to them. They do not automatically understand every security requirement of an application.
For example, a developer might ask an AI tool to create an endpoint that returns customer information.
The generated endpoint may retrieve the correct customer record and return valid JSON. However, it might not check whether the authenticated user is allowed to access that customer's information.
The feature works, but the authorization rule is missing.
Several problems can appear in generated code:
SQL queries built using string concatenation.
Missing authorization checks.
Sensitive information written to application logs.
Hardcoded API keys or passwords.
Unsafe file uploads.
Inadequate input validation.
Weak cryptographic implementations.
Insecure HTTP requests.
Dependencies containing known vulnerabilities.
Error handling that exposes internal information.
These problems are not unique to AI-generated code. They are common software security weaknesses that can appear in any application.
The difference is that AI tools can generate large amounts of code quickly. If developers accept those changes without careful review, a security problem can enter the codebase before anyone notices it.
Start With the Pull Request, Not Just the Generated Code
The most effective security review begins before the code is merged.
Consider an ASP.NET Core application where an AI agent has been asked to create a customer search endpoint.
The agent modifies several files:
CustomerApi/
│
├── Controllers/
│ └── CustomersController.cs
│
├── Services/
│ └── CustomerService.cs
│
├── Data/
│ └── ApplicationDbContext.cs
│
└── Program.cs
Do not review only the controller. Check the complete pull request, including:
New files.
Modified files.
Deleted files.
Project configuration changes.
Package additions and updates.
Database migrations.
Authentication and authorization configuration.
CI/CD workflow changes.
A change that appears harmless in a controller might introduce a new dependency or change the application's authentication configuration.
Start by understanding what the AI agent was asked to do and compare that requirement with the actual changes.
If the task was to add customer search, unexpected changes to authentication middleware or database permissions deserve additional investigation.
Check for SQL Injection
SQL injection occurs when untrusted input is inserted into a SQL statement in a way that changes the intended query.
AI-generated code can introduce this problem when it builds SQL statements using string interpolation or concatenation.
Consider the following example:
public async Task<Customer?> GetCustomerAsync(
string email)
{
var sql = $"""
SELECT *
FROM Customers
WHERE Email = '{email}'
""";
return await _dbContext.Customers
.FromSqlRaw(sql)
.FirstOrDefaultAsync();
}
The email value comes from the caller. If an attacker supplies SQL syntax instead of an ordinary email address, the resulting query may behave differently from what the developer intended.
Use parameterized queries instead:
public async Task<Customer?> GetCustomerAsync(
string email)
{
return await _dbContext.Customers
.FromSqlInterpolated(
$"""
SELECT *
FROM Customers
WHERE Email = {email}
""")
.FirstOrDefaultAsync();
}
Entity Framework Core parameterizes the interpolated value when using FromSqlInterpolated.
For ordinary queries, LINQ is often simpler:
public async Task<Customer?> GetCustomerAsync(
string email)
{
return await _dbContext.Customers
.FirstOrDefaultAsync(
customer => customer.Email == email);
}
During review, search for methods such as FromSqlRaw, ExecuteSqlRaw, and dynamically constructed SQL strings.
These methods are not automatically unsafe, but inspect how user-controlled values are passed to them.
Verify Authentication and Authorization
Authentication answers the question: who is making the request?
Authorization answers a different question: what is that user allowed to access?
AI-generated endpoints sometimes include authentication but miss resource-level authorization.
Consider this endpoint:
[Authorize]
[HttpGet("{customerId:int}")]
public async Task<IActionResult> GetCustomer(
int customerId)
{
var customer = await _context.Customers
.FindAsync(customerId);
if (customer == null)
{
return NotFound();
}
return Ok(customer);
}
The [Authorize] attribute requires an authenticated user. However, it does not prove that the user is allowed to view the requested customer.
An authenticated user might change the ID in the URL and access another customer's information.
This is an example of broken object-level authorization.
A better implementation checks ownership or another explicit access rule:
[Authorize]
[HttpGet("{customerId:int}")]
public async Task<IActionResult> GetCustomer(
int customerId)
{
var userId = User.FindFirst(
System.Security.Claims.ClaimTypes.NameIdentifier
)?.Value;
if (string.IsNullOrWhiteSpace(userId))
{
return Unauthorized();
}
var customer = await _context.Customers
.FirstOrDefaultAsync(
customer =>
customer.Id == customerId &&
customer.UserId == userId);
if (customer == null)
{
return NotFound();
}
return Ok(customer);
}
The exact ownership rule depends on the application. In an enterprise system, access might depend on a tenant, department, role, or resource policy.
During review, check whether every sensitive operation enforces the correct access rule. Do not assume that adding [Authorize] to a controller solves every authorization problem.
Inspect Input Validation and File Handling
Input validation protects an application from malformed, unexpected, or potentially dangerous data.
AI-generated code may validate that a value is present without checking whether it is within acceptable limits.
For example, a file upload endpoint might accept any uploaded file:
[HttpPost("upload")]
public async Task<IActionResult> Upload(
IFormFile file)
{
var path = Path.Combine(
"uploads",
file.FileName);
await using var stream = System.IO.File.Create(path);
await file.CopyToAsync(stream);
return Ok();
}
This implementation has several problems:
It does not check whether a file was supplied.
It trusts the original filename.
It does not enforce a size limit.
It does not validate the file's actual content.
It may allow path manipulation through the filename.
It writes directly to a location without defining an appropriate storage policy.
A safer starting point is to generate a server-controlled filename and enforce basic limits:
[HttpPost("upload")]
[RequestSizeLimit(5 * 1024 * 1024)]
public async Task<IActionResult> Upload(
IFormFile file,
CancellationToken cancellationToken)
{
if (file == null || file.Length == 0)
{
return BadRequest("A file is required.");
}
if (file.Length > 5 * 1024 * 1024)
{
return BadRequest("File size exceeds the limit.");
}
var allowedExtensions = new[]
{
".pdf",
".jpg",
".png"
};
var extension = Path.GetExtension(file.FileName);
if (!allowedExtensions.Contains(
extension,
StringComparer.OrdinalIgnoreCase))
{
return BadRequest("Unsupported file type.");
}
var fileName = $"{Guid.NewGuid()}{extension}";
var uploadDirectory = Path.Combine(
Directory.GetCurrentDirectory(),
"uploads");
Directory.CreateDirectory(uploadDirectory);
var path = Path.Combine(
uploadDirectory,
fileName);
await using var stream = System.IO.File.Create(path);
await file.CopyToAsync(
stream,
cancellationToken);
return Ok(new { FileName = fileName });
}
This is an improved starting point, not a complete production upload solution. File extensions alone do not establish file safety. A production implementation should also validate file signatures, use malware scanning where appropriate, store uploads outside the application execution directory or in object storage, and prevent direct execution of uploaded content.
The review should consider the complete upload process rather than only the controller method.
Look for Hardcoded Secrets and Sensitive Information
AI tools may generate configuration examples containing values that look like real credentials.
For example:
var connectionString =
"Server=prod-db;User Id=admin;Password=MyPassword123;";
Never commit database passwords, API keys, access tokens, or private certificates to source control.
Use configuration providers and a managed secret store instead.
For local development, developers can use .NET User Secrets:
dotnet user-secrets init
dotnet user-secrets set \
"ConnectionStrings:DefaultConnection" \
"YourLocalConnectionString"
The application can read the value through configuration:
var connectionString =
builder.Configuration.GetConnectionString(
"DefaultConnection");
For production, use an appropriate secret-management service, such as Azure Key Vault, and authenticate through managed identity where supported.
Also inspect logging statements.
This is unsafe:
_logger.LogInformation(
"User login request: {Email}, {Password}",
email,
password);
Passwords, access tokens, payment information, and other sensitive values should not appear in logs.
Prefer logging useful diagnostic information without exposing secrets:
_logger.LogInformation(
"Login attempt received for user {Email}",
email);
Even email addresses may be personal information. Apply the application's data-handling and logging policies before recording them.
Review Dependencies Before Merging
AI coding tools often suggest new NuGet packages to implement a feature.
A package may save development time, but every dependency introduces another component that must be maintained and trusted.
Before accepting a new package, check:
Whether the package is maintained.
Whether its version is supported.
Whether it has known security vulnerabilities.
Whether it introduces additional dependencies.
Whether the application already has a suitable alternative.
Whether its license is compatible with the project.
Run the built-in NuGet vulnerability check:
dotnet list package --vulnerable --include-transitive
This checks direct and transitive dependencies for known vulnerabilities.
You can also enable vulnerability auditing during package restore:
<PropertyGroup>
<NuGetAudit>true</NuGetAudit>
</PropertyGroup>
For repositories using GitHub, Dependabot can identify vulnerable dependencies and suggest updates. GitHub code scanning can also identify certain security weaknesses in source code.
A clean dependency report does not prove that a package is safe. It only means that the scanner did not identify a known vulnerability in the checked package versions.
Automate Security Checks in the Pull Request
Manual review is important, but developers should not depend on it alone.
Add automated security checks to the pull request process so that common problems are detected before code reaches the default branch.
A useful pipeline includes:
Build and unit tests.
Static application security testing.
Dependency vulnerability scanning.
Secret detection.
Infrastructure configuration checks, where applicable.
Review of security-sensitive code changes.
For a GitHub repository, GitHub Code Security can provide code scanning through CodeQL, dependency review, and secret scanning, depending on the repository's plan and configuration.
A basic CodeQL workflow for a C# repository can look like this:
name: Code Security Scan
on:
push:
branches:
- main
pull_request:
branches:
- main
schedule:
- cron: '30 2 * * 1'
jobs:
analyze:
name: Analyze CSharp
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: csharp
- name: Build application
uses: github/codeql-action/autobuild@v3
- name: Analyze code
uses: github/codeql-action/analyze@v3
This workflow illustrates the main CodeQL steps. Use action versions approved by your organization and verify compatibility with your repository's current GitHub Actions configuration.
For a production repository, configure the security workflow as a required status check where appropriate. Also review the alert severity and establish which findings should block a merge.
Do not automatically ignore every warning. If a finding is a false positive, document the reason and follow the team's exception process.
Test Security Behavior, Not Just Successful Execution
Unit tests usually verify whether a method returns the expected result for a given input. Security testing must also verify that unauthorized or malicious inputs are rejected.
For the customer endpoint, write a test that checks whether one user can access another user's customer record.
For example:
[Fact]
public async Task GetCustomer_ReturnsNotFound_ForAnotherUsersRecord()
{
var customer = new Customer
{
Id = 100,
UserId = "user-1",
Name = "Test Customer"
};
await SeedCustomerAsync(customer);
var client = await CreateAuthenticatedClientAsync(
"user-2");
var response = await client.GetAsync(
"/api/customers/100");
Assert.Equal(
HttpStatusCode.NotFound,
response.StatusCode);
}
The helper methods represent the application's test setup. The important part is that the test authenticates as a different user and attempts to access the customer record.
Add similar tests for:
Missing authentication.
Insufficient permissions.
Invalid input.
Oversized file uploads.
Unexpected file types.
Expired or invalid tokens.
Attempts to access another tenant's data.
Security tests should reflect the actual authorization and data-handling rules of the application.
Use AI to Review AI-Generated Code
A second AI review can help identify issues that the original code-generation task missed.
However, asking an AI tool whether its own code is secure is not sufficient. Give it a specific review scope and ask for evidence from the changed files.
For example:
Review the changes in this pull request
for security problems.
Check specifically for:
1. SQL injection.
2. Missing authentication or authorization.
3. Insecure file handling.
4. Hardcoded secrets.
5. Sensitive information in logs.
6. Unsafe external HTTP requests.
7. Insecure dependency changes.
8. Missing validation of user input.
For every finding, provide:
- The affected file and method.
- The relevant code.
- The security risk.
- A practical correction.
Do not modify any files.
Do not report an issue unless you can
identify the relevant code.
This makes the review more focused and easier to verify.
Treat the AI review as an additional source of findings, not a security certification. Confirm each issue through code inspection, tests, or a suitable security tool.
Common Mistakes to Avoid
Reviewing only the generated method
Security problems can appear in configuration, dependencies, middleware, and other files changed by the agent. Review the entire pull request.
Trusting passing unit tests
Tests may verify expected functionality without checking unauthorized access, malicious input, or sensitive data exposure.
Accepting new dependencies without investigation
A package may introduce vulnerabilities, unnecessary permissions, or an additional maintenance burden.
Ignoring security alerts to merge faster
A critical finding should be investigated before merging. If an alert is a false positive, document the evidence and use the approved exception process.
Treating AI review as a replacement for human review
AI tools can miss vulnerabilities and misunderstand business-specific security requirements. Keep a human reviewer responsible for approving sensitive changes.
Summary
AI-generated code should follow the same security standards as manually written code. Passing a build and test suite does not prove that a change is safe.
Before merging, inspect the complete pull request, check database queries, verify authorization, validate external input, review file handling, and search for exposed secrets. Scan dependencies and use automated code analysis to catch common weaknesses.
Security testing should also include negative cases, such as unauthorized access and invalid input. An additional AI review can help find problems, but every finding must be verified.
The goal is not to slow down AI-assisted development. It is to make sure that faster code generation does not bypass the security checks that protect production applications.
Join the conversation! Your thoughts help the community grow.