validate encoded data (#544)

* validate encoded data

* u

* c
This commit is contained in:
Felicio Mununga 2024-04-08 16:21:17 +09:00 committed by GitHub
parent 98741fb49c
commit f9af5e0216
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 211 additions and 9 deletions

View File

@ -0,0 +1,5 @@
---
'@status-im/js': patch
---
validate encoded data

View File

@ -32,6 +32,33 @@ describe('Encode URL data', () => {
expect(decodedData).toEqual(data) expect(decodedData).toEqual(data)
}) })
test('should throw for invalid community data', () => {
expect(() => {
const encodedData = 'Ow=='
decodeCommunityURLData(encodedData)
}).toThrowError()
})
test('should throw for unsupported data length', () => {
expect(() => {
const encodedData =
'G2QBQJwFdqwxrBnNb57kP0irrJpuouIjS1WZqHS6A2txojsUHidyu3evaAO3GQQku5NCQXiwAYchBIMNyptts=MD9bZAwoTasraIMkjbS1uAD7oxsAQ53OAmQWCefyBuuXlAu6J7eKQRQhgg5tan75fFp9jwGIjBLbGhnyUht2qj5GWlSBp7_OXsHxgnr21xA2HgR9VGYYikQJA4tcQHDrQzg_ARC9KiOVDD6vgTCM9_CN0HJ1zxwP3w6nzgkDTNuvDCFD3Clqo6Cf_UNY2cNRlKTqj86G4gC2dUNSApwiq72BdGTtrleiRFPUhCbTRbmEG4YwFOs4EjBdJHHRiqjS5GYGc1dAdgcGr2BQ==============================================================================================================================================='
decodeCommunityURLData(encodedData)
}).toThrowErrorMatchingInlineSnapshot(`
"[
{
\\"code\\": \\"too_big\\",
\\"maximum\\": 500,
\\"type\\": \\"string\\",
\\"inclusive\\": true,
\\"exact\\": false,
\\"message\\": \\"String must contain at most 500 character(s)\\",
\\"path\\": []
}
]"
`)
})
test('should encode and decode channel', () => { test('should encode and decode channel', () => {
const data = { const data = {
emoji: '🏴󠁧󠁢󠁥󠁮󠁧󠁿', emoji: '🏴󠁧󠁢󠁥󠁮󠁧󠁿',
@ -56,6 +83,70 @@ describe('Encode URL data', () => {
expect(decodedData).toEqual(data) expect(decodedData).toEqual(data)
}) })
test('should throw for invalid channel data', () => {
expect(() => {
const encodedData = 'Ow=='
decodeChannelURLData(encodedData)
}).toThrowErrorMatchingInlineSnapshot(`
"[
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"displayName\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"description\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"emoji\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"color\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"object\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"community\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"uuid\\"
],
\\"message\\": \\"Required\\"
}
]"
`)
})
test('should encode and decode user', () => { test('should encode and decode user', () => {
const data = { const data = {
displayName: 'Lorem ipsum dolore nulla', displayName: 'Lorem ipsum dolore nulla',
@ -72,4 +163,68 @@ describe('Encode URL data', () => {
) )
expect(decodedData).toEqual(data) expect(decodedData).toEqual(data)
}) })
test('should throw for invalid user data', () => {
expect(() => {
const encodedData = 'Ow=='
decodeChannelURLData(encodedData)
}).toThrowErrorMatchingInlineSnapshot(`
"[
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"displayName\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"description\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"emoji\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"color\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"object\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"community\\"
],
\\"message\\": \\"Required\\"
},
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"uuid\\"
],
\\"message\\": \\"Required\\"
}
]"
`)
})
}) })

View File

@ -1,5 +1,6 @@
import { base64url } from '@scure/base' import { base64url } from '@scure/base'
import { brotliCompressSync, brotliDecompressSync } from 'zlib' import { brotliCompressSync, brotliDecompressSync } from 'zlib'
import { z } from 'zod'
import { Channel, Community, URLData, User } from '../protos/url_pb' import { Channel, Community, URLData, User } from '../protos/url_pb'
@ -7,6 +8,17 @@ import type { PlainMessage } from '@bufbuild/protobuf'
export type EncodedURLData = string & { _: 'EncodedURLData' } export type EncodedURLData = string & { _: 'EncodedURLData' }
const colorSchema = z.string().regex(/^#[0-9A-Fa-f]{6}$/)
const communityDisplayName = z.string().max(30).nonempty()
const communitySchema = z.object({
displayName: communityDisplayName,
description: z.string().max(140).nonempty(),
membersCount: z.number().nonnegative(),
color: colorSchema,
tagIndices: z.number().nonnegative().array(),
})
export function encodeCommunityURLData( export function encodeCommunityURLData(
data: PlainMessage<Community> data: PlainMessage<Community>
): EncodedURLData { ): EncodedURLData {
@ -17,25 +29,48 @@ export function encodeCommunityURLData(
export function decodeCommunityURLData(data: string): PlainMessage<Community> { export function decodeCommunityURLData(data: string): PlainMessage<Community> {
const deserialized = decodeURLData(data) const deserialized = decodeURLData(data)
return Community.fromBinary( const community = Community.fromBinary(deserialized.content).toJson()
deserialized.content
).toJson() as PlainMessage<Community> return communitySchema.parse(community)
} }
const channelSchema = z.object({
displayName: z.string().max(24).nonempty(),
description: z.string().max(140).nonempty(),
emoji: z.string().emoji(),
color: colorSchema,
community: z.object({
displayName: communityDisplayName,
}),
uuid: z.string().uuid(),
})
export function encodeChannelURLData( export function encodeChannelURLData(
data: PlainMessage<Channel> data: PlainMessage<Channel>
): EncodedURLData { ): EncodedURLData {
return encodeURLData(new Channel(data).toBinary()) as EncodedURLData return encodeURLData(new Channel(data).toBinary()) as EncodedURLData
} }
export function decodeChannelURLData(data: string): PlainMessage<Channel> { export function decodeChannelURLData(data: string): Omit<
PlainMessage<Channel>,
'community'
> & {
community: Pick<PlainMessage<Community>, 'displayName'>
} {
const deserialized = decodeURLData(data) const deserialized = decodeURLData(data)
return Channel.fromBinary( const channel = Channel.fromBinary(deserialized.content).toJson()
deserialized.content
).toJson() as PlainMessage<Channel> return channelSchema.parse(channel)
} }
const userSchema = z.object({
displayName: z.string().max(24).nonempty(),
description: z.string().max(240).nonempty(),
// fixme: await integration in native platforms
color: colorSchema.optional().default('#ffffff'),
})
export function encodeUserURLData(data: PlainMessage<User>): EncodedURLData { export function encodeUserURLData(data: PlainMessage<User>): EncodedURLData {
return encodeURLData(new User(data).toBinary()) as EncodedURLData return encodeURLData(new User(data).toBinary()) as EncodedURLData
} }
@ -43,7 +78,9 @@ export function encodeUserURLData(data: PlainMessage<User>): EncodedURLData {
export function decodeUserURLData(data: string): PlainMessage<User> { export function decodeUserURLData(data: string): PlainMessage<User> {
const deserialized = decodeURLData(data) const deserialized = decodeURLData(data)
return User.fromBinary(deserialized.content).toJson() as PlainMessage<User> const user = User.fromBinary(deserialized.content).toJson()
return userSchema.parse(user)
} }
function encodeURLData(data: Uint8Array): string { function encodeURLData(data: Uint8Array): string {
@ -57,6 +94,11 @@ function encodeURLData(data: Uint8Array): string {
} }
function decodeURLData(data: string): URLData { function decodeURLData(data: string): URLData {
// note: https://github.com/status-im/status-web/pull/345#discussion_r1113129396 observed lengths
// note?: https://docs.google.com/spreadsheets/d/1JD4kp0aUm90piUZ7FgM_c2NGe2PdN8BFB11wmt5UZIY/view#gid=1260088614 limit for url path segmets not split by ";" or "_"
// fixme: set to 301 per url path segment when the above mentioned splitting is implemented
z.string().max(500).parse(data) // default max in order not to compute arbitrary values
const decoded = base64url.decode(data) const decoded = base64url.decode(data)
const decompressed = brotliDecompressSync(decoded) const decompressed = brotliDecompressSync(decoded)
const deserialized = URLData.fromBinary(decompressed) const deserialized = URLData.fromBinary(decompressed)