Skip to content

Commit 1452d6c

Browse files
authored
Merge pull request #1148 from Fury03/fix/1098-validate-stellar-public-key
fix(backend): validate Stellar publicKey format in user validator
2 parents ca6cab0 + 5ca0180 commit 1452d6c

2 files changed

Lines changed: 93 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: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { ZodError } from 'zod';
3+
import {
4+
registerUserSchema,
5+
STELLAR_PUBLIC_KEY_ERROR,
6+
} from '../src/validators/user.validator.js';
7+
8+
const VALID_KEY = 'GD2XP6FNWL6IWULVMPNA2RV2T7GLCJHK3RH75GBCY7TSVIWDITJN4FXJ';
9+
10+
describe('User Validator', () => {
11+
it('should accept a well-formed Stellar public key', () => {
12+
const result = registerUserSchema.safeParse({ publicKey: VALID_KEY });
13+
expect(result.success).toBe(true);
14+
});
15+
16+
it('should reject a malformed public key with a descriptive message', () => {
17+
const result = registerUserSchema.safeParse({ publicKey: 'not-a-stellar-key' });
18+
19+
expect(result.success).toBe(false);
20+
expect(result.error?.issues).toHaveLength(1);
21+
expect(result.error?.issues[0]?.path).toEqual(['publicKey']);
22+
expect(result.error?.issues[0]?.message).toBe(STELLAR_PUBLIC_KEY_ERROR);
23+
});
24+
25+
it.each([
26+
['too short', 'GD2XP6FNWL6IWULV'],
27+
['too long', `${VALID_KEY}AAAA`],
28+
['using a wrong version prefix', `S${VALID_KEY.slice(1)}`],
29+
['lowercase', VALID_KEY.toLowerCase()],
30+
['outside the base32 alphabet', `${VALID_KEY.slice(0, 55)}1`],
31+
['an empty string', ''],
32+
['whitespace padded', ` ${VALID_KEY} `],
33+
])('should reject a key that is %s', (_label, publicKey) => {
34+
const result = registerUserSchema.safeParse({ publicKey });
35+
36+
expect(result.success).toBe(false);
37+
expect(result.error?.issues[0]?.message).toBe(STELLAR_PUBLIC_KEY_ERROR);
38+
});
39+
40+
it.each([
41+
['missing', undefined],
42+
['a number', 12345],
43+
['null', null],
44+
])('should reject a publicKey that is %s', (_label, publicKey) => {
45+
const result = registerUserSchema.safeParse({ publicKey });
46+
47+
expect(result.success).toBe(false);
48+
expect(result.error?.issues[0]?.message).toBe(
49+
'publicKey is required and must be a string',
50+
);
51+
});
52+
53+
// `registerUser` calls `.parse`, so a malformed key throws before any Prisma
54+
// access. The global error middleware turns that ZodError into a 400 carrying
55+
// the message asserted above (see tests/error.middleware.test.ts).
56+
it('should throw a ZodError so the request fails before the controller body runs', () => {
57+
const parse = () => registerUserSchema.parse({ publicKey: 'not-a-stellar-key' });
58+
59+
expect(parse).toThrow(ZodError);
60+
61+
try {
62+
parse();
63+
expect.unreachable('parse should have thrown');
64+
} catch (error) {
65+
expect((error as ZodError).issues[0]?.message).toBe(STELLAR_PUBLIC_KEY_ERROR);
66+
}
67+
});
68+
});

0 commit comments

Comments
 (0)