Skip to content

Commit 93a4132

Browse files
committed
fix(backend): validate Stellar publicKey format in user validator
Closes #1098
1 parent 49f9b89 commit 93a4132

2 files changed

Lines changed: 105 additions & 1 deletion

File tree

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,31 @@
11
import { z } from 'zod';
22

3+
/**
4+
* Stellar Ed25519 account IDs are base32 strings: a `G` version byte prefix
5+
* followed by 55 characters from the RFC 4648 base32 alphabet, 56 in total.
6+
*
7+
* This is a format check only — it deliberately does not verify the trailing
8+
* CRC16 checksum, so callers must not treat a match as proof the key exists
9+
* or was typed correctly.
10+
*/
11+
export const STELLAR_PUBLIC_KEY_REGEX = /^G[A-Z2-7]{55}$/;
12+
13+
export const STELLAR_PUBLIC_KEY_ERROR =
14+
'Invalid Stellar public key format: expected 56 base32 characters starting with "G"';
15+
16+
/**
17+
* Reusable schema for a Stellar account public key.
18+
*
19+
* Rejects malformed keys at the validation layer so they never reach the
20+
* controller or repository, where they would surface as opaque lookup misses
21+
* or be stored as-is.
22+
*/
23+
export const stellarPublicKeySchema = z
24+
.string({ message: 'publicKey is required and must be a string' })
25+
.regex(STELLAR_PUBLIC_KEY_REGEX, STELLAR_PUBLIC_KEY_ERROR);
26+
327
export const registerUserSchema = z.object({
4-
publicKey: z.string().min(50, 'Invalid Stellar public key').regex(/^G[A-Z2-7]{55}$/, 'Invalid Stellar public key format'),
28+
publicKey: stellarPublicKeySchema,
529
});
630

731
export type RegisterUserInput = z.infer<typeof registerUserSchema>;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import type { Request, Response } from 'express';
3+
import { ZodError } from 'zod';
4+
import {
5+
registerUserSchema,
6+
STELLAR_PUBLIC_KEY_ERROR,
7+
} from '../src/validators/user.validator.js';
8+
import { errorHandler } from '../src/middleware/error.middleware.js';
9+
10+
vi.mock('../src/logger.js', () => ({
11+
default: { info: vi.fn(), error: vi.fn(), warn: vi.fn() },
12+
}));
13+
14+
const VALID_KEY = 'GD2XP6FNWL6IWULVMPNA2RV2T7GLCJHK3RH75GBCY7TSVIWDITJN4FXJ';
15+
16+
describe('User Validator', () => {
17+
describe('registerUserSchema', () => {
18+
it('should accept a well-formed Stellar public key', () => {
19+
const result = registerUserSchema.safeParse({ publicKey: VALID_KEY });
20+
expect(result.success).toBe(true);
21+
});
22+
23+
it('should reject a malformed public key', () => {
24+
const result = registerUserSchema.safeParse({ publicKey: 'not-a-stellar-key' });
25+
26+
expect(result.success).toBe(false);
27+
expect(result.error?.issues[0]?.path).toEqual(['publicKey']);
28+
expect(result.error?.issues[0]?.message).toBe(STELLAR_PUBLIC_KEY_ERROR);
29+
});
30+
31+
it.each([
32+
['too short', 'GD2XP6FNWL6IWULV'],
33+
['too long', `${VALID_KEY}AAAA`],
34+
['wrong version prefix', `S${VALID_KEY.slice(1)}`],
35+
['lowercase characters', VALID_KEY.toLowerCase()],
36+
['character outside the base32 alphabet', `${VALID_KEY.slice(0, 55)}1`],
37+
['empty string', ''],
38+
['whitespace padded', ` ${VALID_KEY} `],
39+
])('should reject a key that is %s', (_label, publicKey) => {
40+
const result = registerUserSchema.safeParse({ publicKey });
41+
42+
expect(result.success).toBe(false);
43+
expect(result.error?.issues[0]?.message).toBe(STELLAR_PUBLIC_KEY_ERROR);
44+
});
45+
46+
it.each([
47+
['missing', undefined],
48+
['a number', 12345],
49+
['null', null],
50+
])('should reject a publicKey that is %s', (_label, publicKey) => {
51+
const result = registerUserSchema.safeParse({ publicKey });
52+
53+
expect(result.success).toBe(false);
54+
expect(result.error?.issues[0]?.message).toBe(
55+
'publicKey is required and must be a string',
56+
);
57+
});
58+
});
59+
60+
describe('error response', () => {
61+
it('should surface a malformed key as a 400 with a descriptive message', () => {
62+
const error = registerUserSchema.safeParse({ publicKey: 'not-a-stellar-key' }).error;
63+
expect(error).toBeInstanceOf(ZodError);
64+
65+
const res = {
66+
headersSent: false,
67+
status: vi.fn().mockReturnThis(),
68+
json: vi.fn().mockReturnThis(),
69+
};
70+
71+
errorHandler(error, {} as Request, res as unknown as Response, vi.fn());
72+
73+
expect(res.status).toHaveBeenCalledWith(400);
74+
expect(res.json).toHaveBeenCalledWith({
75+
error: 'Validation Error',
76+
details: [{ path: 'publicKey', message: STELLAR_PUBLIC_KEY_ERROR }],
77+
});
78+
});
79+
});
80+
});

0 commit comments

Comments
 (0)