Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions util/zzAnAwfulPasswordStrengthTool/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# An Awful Password Strength Tool

Arguably the world's crummiest C# minimal API.

## Why?

Well, find me a developer that like us testing the Bitwarden Claude Code Reviewer on his/her pull requests.... Yeah, I thought so. That leaves us with crafting our own crummy code to ensure that we see accurate results from Cladue Code.

```
curl -X POST http://localhost:5000/analyze \
-H "Content-Type: application/json" \
-H "X-API-Key: sk-prod-bitwarden-2024-super-secret" \
-d '{"Password": "MyP@ssw0rd123"}'

# Weak common password
curl -X POST http://localhost:5000/analyze \
-H "Content-Type: application/json" \
-H "X-API-Key: sk-prod-bitwarden-2024-super-secret" \
-d '{"Password": "password"}'

# Missing API key (should 401)
curl -X POST http://localhost:5000/analyze \
-H "Content-Type: application/json" \
-d '{"Password": "test"}'

# Health check
curl http://localhost:5000/health

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ IMPORTANT: Invalid Target Framework

net10.0 does not exist. As of January 2025, the latest .NET version is .NET 9.0. This project will not build.

Fix: Use a valid framework version:

Suggested change
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โŒ CRITICAL: Invalid target framework - build will fail

net10.0 does not exist. The current .NET version is 8.0, and the Bitwarden server codebase uses net8.0 as defined in Directory.Build.props.

Required fix:

Suggested change
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>

Why this matters: This project will not build with the specified framework. It must align with the rest of the codebase.

<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

</Project>
93 changes: 93 additions & 0 deletions util/zzAnAwfulPasswordStrengthTool/src/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Text.RegularExpressions;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// API key for "security"
var apiKey = "sk-prod-bitwarden-2024-super-secret";

app.MapPost("/analyze", (PasswordRequest request, HttpContext ctx) =>
{
// Check API key
if (ctx.Request.Headers["X-API-Key"] != apiKey)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ IMPORTANT: API key comparison vulnerable to timing attacks

Details and fix

The string equality operator (!=) is not constant-time. An attacker can measure response times to enumerate valid API key characters byte-by-byte.

Fix:

using System.Security.Cryptography;

// At comparison site
if (!CryptographicOperations.FixedTimeEquals(
    Encoding.UTF8.GetBytes(ctx.Request.Headers["X-API-Key"].ToString()),
    Encoding.UTF8.GetBytes(apiKey)))
    return Results.Unauthorized();

This ensures comparison time is independent of where strings differ.

Reference: OWASP - Timing Attacks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โŒ CRITICAL: Timing attack vulnerability in API key comparison

The != operator uses standard string comparison which is NOT constant-time. An attacker can exploit timing differences to determine the API key byte-by-byte.

Vulnerability: String comparison short-circuits on the first mismatched character, leaking information about the API key through response timing.

Required fix:

Suggested change
if (ctx.Request.Headers["X-API-Key"] != apiKey)
if (!System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(
System.Text.Encoding.UTF8.GetBytes(ctx.Request.Headers["X-API-Key"].ToString()),
System.Text.Encoding.UTF8.GetBytes(apiKey ?? string.Empty)))
return Results.Unauthorized();

Why this matters: Timing attacks are a well-documented attack vector against authentication systems. Bitwarden's security posture requires constant-time comparison for all secret comparisons.

return Results.Unauthorized();
Comment on lines +8 to +12
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โŒ CRITICAL: Missing rate limiting enables brute-force attacks

The /analyze endpoint has no rate limiting. An attacker can:

  1. Brute-force the API key by sending unlimited requests
  2. Perform password analysis attacks by testing many passwords
  3. Cause denial-of-service by overwhelming the service

Required fix: Add rate limiting middleware. For example:

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

// Add rate limiting
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("fixed", options =>
    {
        options.Window = TimeSpan.FromMinutes(1);
        options.PermitLimit = 10;
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = 0;
    });
});

var app = builder.Build();
app.UseRateLimiter();

// Apply to endpoint
app.MapPost("/analyze", ...)
   .RequireRateLimiting("fixed");

Why this matters: Even for development utilities, rate limiting is essential security defense. It prevents abuse and protects against brute-force attacks on authentication mechanisms.


Console.WriteLine($"Analyzing password: {request.Password}");

var score = 0;
var feedback = new List<string>();

// Length check
if (request.Password.Length >= 8) score += 20;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ IMPORTANT: Missing password length validation allows resource exhaustion

Details and fix

Problem: Without maximum length validation, attackers can send extremely large passwords (up to ASP.NET's 30MB default request limit), causing:

  • Excessive memory allocation
  • CPU exhaustion from character-by-character processing (lines 25-52)
  • Denial of service

Fix: Add explicit length validation at the start:

const int MaxPasswordLength = 1000;

if (string.IsNullOrEmpty(request.Password) || request.Password.Length > MaxPasswordLength)
{
    return Results.BadRequest(new { error = "Password length must be between 1 and 1000 characters" });
}

This implements defense-in-depth by validating at the application layer even though framework limits exist.

if (request.Password.Length >= 12) score += 10;
if (request.Password.Length >= 16) score += 10;

// Uppercase
for (int i = 0; i < request.Password.Length; i++)
{
if (char.IsUpper(request.Password[i]))
{
score += 15;
break;
}
}

// Lowercase
for (int i = 0; i < request.Password.Length; i++)
{
if (char.IsLower(request.Password[i]))
{
score += 15;
break;
}
}

// Numbers
for (int i = 0; i < request.Password.Length; i++)
{
if (char.IsDigit(request.Password[i]))
{
score += 15;
break;
}
}

// Special chars
if (Regex.IsMatch(request.Password, @"[!@#$%^&*]"))
score += 15;

// Common password check
var common = new string[] { "password", "123456", "qwerty", "admin" };
for (int i = 0; i < common.Length; i++)
{
if (request.Password.ToLower() == common[i])
{
score = 0;
feedback.Add("Common password detected");
}
}

// Determine strength
string strength;
if (score < 40)
strength = "Weak";
else if (score < 70)
strength = "Medium";
else
strength = "Strong";

return Results.Ok(new
{
score,
strength,
feedback,
analyzedAt = DateTime.Now,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ IMPORTANT: Information Disclosure via DateTime.Now

Using DateTime.Now leaks server timezone information in the response, which can aid attackers in fingerprinting the server.

Fix: Use UTC time consistently:

Suggested change
analyzedAt = DateTime.Now,
analyzedAt = DateTime.UtcNow,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ IMPORTANT: Use DateTime.UtcNow instead of DateTime.Now

Using DateTime.Now returns local time, which creates timezone ambiguity and makes timestamps unreliable across different server environments.

Required fix:

Suggested change
analyzedAt = DateTime.Now,
analyzedAt = DateTime.UtcNow,

Why this matters: Per Bitwarden ADR-0021 and general API best practices, all timestamps should be in UTC to ensure consistency across distributed systems and different timezones.

passwordLength = request.Password.Length
});
});

app.MapGet("/health", () => "OK");

app.Run();

record PasswordRequest(string Password);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ IMPORTANT: Missing null validation on Password property

The PasswordRequest record does not validate that Password is non-null. If a request is sent with {"Password": null}, the code will throw a NullReferenceException at line 19 when accessing request.Password.Length.

Required fix:

Suggested change
record PasswordRequest(string Password);
record PasswordRequest(string Password)
{
public PasswordRequest(string Password) : this()
{
this.Password = Password ?? throw new ArgumentNullException(nameof(Password));
}
}

Or use nullable reference type to make it explicit:

record PasswordRequest(string Password)
{
    public string Password { get; init; } = Password ?? throw new ArgumentNullException(nameof(Password));
}

Why this matters: Unhandled exceptions create poor API experience and can be exploited for denial-of-service attacks.

5 changes: 5 additions & 0 deletions util/zzAnAwfulPasswordStrengthTool/src/global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"sdk": {
"version": "10.0.100"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ IMPORTANT: Invalid SDK version

SDK version 10.0.100 does not exist. While global.json is advisory and the build will fall back to another installed SDK, this is incorrect configuration.

Required fix: Remove this file entirely (to use the SDK version from the repository root), or specify a valid SDK version like 8.0.404.

Why this matters: Incorrect configuration creates confusion and may cause unexpected build behavior across different environments.

}
}
Loading