Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 10 additions & 0 deletions .changeset/fix-vulnerable-deps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@csrf-armor/core": patch
"@csrf-armor/express": patch
"@csrf-armor/nextjs": patch
"@csrf-armor/nuxt": patch
---

fix: resolve high/moderate severity vulnerabilities in transitive dependencies

Added pnpm overrides to force patched versions of `lodash` (>=4.18.0) and `defu` (>=6.1.5), which were pulled in transitively through the nuxt dependency chain. Addresses GHSA-r5fr-rjxr-66jc (lodash code injection), GHSA-f23m-r3pf-42rh (lodash prototype pollution), and GHSA-737v-mqg7-c878 (defu prototype pollution).
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
"description": "Framework-agnostic CSRF protection library",
"type": "module",
"private": true,
"author": "Muneeb Samuels",
"contributors": [
{ "name": "Raul", "url": "https://github.com/raulcrisan" },
{ "name": "Jordan Labrosse", "url": "https://github.com/Jorgagu" }
],
"workspaces": [
"packages/*"
],
Expand Down Expand Up @@ -65,7 +70,9 @@
"path-to-regexp@>=0.1.0 <0.2.0": "0.1.13",
"minimatch@>=5.0.0 <6.0.0": "5.1.9",
"minimatch@>=9.0.0 <10.0.0": "9.0.9",
"minimatch@>=10.0.0 <11.0.0": "10.2.4"
"minimatch@>=10.0.0 <11.0.0": "10.2.4",
"lodash": ">=4.18.0",
"defu": ">=6.1.5"
}
},
"packageManager": "pnpm@10.2.1+sha512.398035c7bd696d0ba0b10a688ed558285329d27ea994804a52bad9167d8e3a72bcb993f9699585d3ca25779ac64949ef422757a6c31102c12ab932e5cbe5cc92"
Expand Down
59 changes: 59 additions & 0 deletions packages/core/tests/crypto.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
generateNonce,
generateSecureSecret,
generateSignedToken,
parseSignedToken,
signUnsignedToken,
timingSafeEqual,
verifySignedToken,
} from '../src';
import { TokenExpiredError, TokenInvalidError } from '../src';
Expand Down Expand Up @@ -133,4 +135,61 @@ describe('Crypto utilities', () => {
);
});
});

describe('timingSafeEqual', () => {
it('should return true for equal strings', () => {
expect(timingSafeEqual('hello', 'hello')).toBe(true);
});

it('should return false for unequal strings', () => {
expect(timingSafeEqual('hello', 'world')).toBe(false);
});

it('should return false for strings of different lengths', () => {
expect(timingSafeEqual('short', 'longer-string')).toBe(false);
});

it('should return true for empty strings', () => {
expect(timingSafeEqual('', '')).toBe(true);
});

it('should detect a single character difference', () => {
expect(timingSafeEqual('abcde', 'abcdf')).toBe(false);
});
});

describe('generateSecureSecret', () => {
it('should return a non-empty string', () => {
const secret = generateSecureSecret();
expect(secret).toBeTruthy();
expect(secret.length).toBeGreaterThan(0);
});

it('should return a base64-encoded string', () => {
const secret = generateSecureSecret();
expect(secret).toMatch(/^[A-Za-z0-9+/=]+$/);
});

it('should generate unique values on each call', () => {
const secret1 = generateSecureSecret();
const secret2 = generateSecureSecret();
expect(secret1).not.toBe(secret2);
});

it('should return a string of 44 characters (32 bytes base64-encoded)', () => {
const secret = generateSecureSecret();
expect(secret).toHaveLength(44);
});
});

describe('parseSignedToken (edge cases)', () => {
it('should throw TokenInvalidError with "Invalid expiration timestamp" for non-numeric exp', async () => {
// "abc" is the expStr — parseInt("abc") is NaN, which is checked BEFORE
// signature verification in parseSignedToken, so this always throws
// regardless of the nonce or signature values.
await expect(
parseSignedToken('abc.nonce.signature', 'secret')
).rejects.toThrow(new TokenInvalidError('Invalid expiration timestamp'));
});
});
});
Loading
Loading