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
1 change: 1 addition & 0 deletions redisinsight/api/config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export default {
modules: {
json: {
sizeThreshold: parseInt(process.env.RI_JSON_SIZE_THRESHOLD, 10) || 1024,
lengthThreshold: parseInt(process.env.RI_JSON_LENGTH_THRESHOLD, 10) || -1,
},
},
redis_cli: {
Expand Down
1 change: 1 addition & 0 deletions redisinsight/api/src/constants/error-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export default {
'Encountered a timeout error while attempting to retrieve data',
RDI_VALIDATION_ERROR: 'Validation error',
INVALID_RDI_INSTANCE_ID: 'Invalid rdi instance id.',
UNSAFE_BIG_JSON_LENGTH: 'This JSON is too large. Try opening it with Redis Insight Desktop.',

// database settings
DATABASE_SETTINGS_NOT_FOUND: 'Could not find settings for this database',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,16 @@ import {
import { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';
import { mockAddFieldsDto } from 'src/modules/browser/__mocks__';
import { DatabaseService } from 'src/modules/database/database.service';
import config, { Config } from 'src/utils/config';
import { RejsonRlService } from './rejson-rl.service';

const mockModulesConfig = config.get('modules') as Config['modules'];

jest.mock(
'src/utils/config',
jest.fn(() => jest.requireActual('src/utils/config') as object),
);

const testKey = Buffer.from('somejson');
const testSerializedObject = JSON.stringify({ some: 'object' });
const testPath = '$';
Expand All @@ -40,6 +48,8 @@ describe('JsonService', () => {
let databaseService: MockType<DatabaseService>;

beforeEach(async () => {
mockModulesConfig.json.lengthThreshold = -1;

const module: TestingModule = await Test.createTestingModule({
providers: [
RejsonRlService,
Expand Down Expand Up @@ -593,6 +603,27 @@ describe('JsonService', () => {
],
});
});
it('should throw an error when array length exceeds threshold', async () => {
mockModulesConfig.json.lengthThreshold = 5_000;

when(client.sendCommand)
.calledWith(
[BrowserToolRejsonRlCommands.JsonType, testKey, testPath],
{ replyEncoding: 'utf8' },
)
.mockReturnValue(['array']);
when(client.sendCommand)
.calledWith(
[BrowserToolRejsonRlCommands.JsonArrLen, testKey, testPath],
{ replyEncoding: 'utf8' },
)
.mockReturnValue([5001]);

await expect(service.getJson(mockBrowserClientMetadata, {
keyName: testKey,
path: testPath,
})).rejects.toThrow(new BadRequestException(ERROR_MESSAGES.UNSAFE_BIG_JSON_LENGTH));
});
it('should return array with scalar values in a custom path', async () => {
const path = '$["customPath"]';
const testData = [12, 'str'];
Expand Down Expand Up @@ -774,6 +805,27 @@ describe('JsonService', () => {
],
});
});
it('should throw an error when number of object entries exceeds threshold', async () => {
mockModulesConfig.json.lengthThreshold = 5_000;

when(client.sendCommand)
.calledWith(
[BrowserToolRejsonRlCommands.JsonType, testKey, testPath],
{ replyEncoding: 'utf8' },
)
.mockReturnValue(['object']);
when(client.sendCommand)
.calledWith(
[BrowserToolRejsonRlCommands.JsonObjLen, testKey, testPath],
{ replyEncoding: 'utf8' },
)
.mockReturnValue([10_000]);

await expect(service.getJson(mockBrowserClientMetadata, {
keyName: testKey,
path: testPath,
})).rejects.toThrow(new BadRequestException(ERROR_MESSAGES.UNSAFE_BIG_JSON_LENGTH));
});
it('should return object with scalar values as strings in a custom path', async () => {
const path = '$["customPath"]';
const testData = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as JSONBigInt from 'json-bigint';
import { AdditionalRedisModuleName, RedisErrorCodes } from 'src/constants';
import ERROR_MESSAGES from 'src/constants/error-messages';
import { catchAclError } from 'src/utils';
import config from 'src/utils/config';
import config, { Config } from 'src/utils/config';
import { ClientMetadata } from 'src/common/models';
import {
CreateRejsonRlWithExpireDto,
Expand All @@ -34,6 +34,7 @@ import {
import { RedisClient } from 'src/modules/redis/client';
import { DatabaseService } from 'src/modules/database/database.service';

const MODULES_CONFIG = config.get('modules') as Config['modules'];
const JSONbig = JSONBigInt();

@Injectable()
Expand Down Expand Up @@ -143,6 +144,45 @@ export class RejsonRlService {
return path[0] === '$' ? type[0] : type;
}

private async isUnsafeBigJsonLength(
client: RedisClient,
keyName: RedisString,
path: string,
) {
const type = await this.getJsonDataType(client, keyName, path);

let length: number | null;

switch (type) {
case 'object':
length = await client.sendCommand(
[BrowserToolRejsonRlCommands.JsonObjLen, keyName, path],
{ replyEncoding: 'utf8' },
) as number;

break;
case 'array':
length = await client.sendCommand(
[BrowserToolRejsonRlCommands.JsonArrLen, keyName, path],
{ replyEncoding: 'utf8' },
) as number;

break;
default:
// for the rest types we can safely continue processing
// Even for big strings since it should be handled by "sizeThreshold" before
return false;
}

if (length === null) {
throw new BadRequestException(
`There is no such path: "${path}" in key: "${keyName}"`,
);
}

return MODULES_CONFIG.json.lengthThreshold > 0 && length > MODULES_CONFIG.json.lengthThreshold;
}

private async getDetails(
client: RedisClient,
keyName: RedisString,
Expand Down Expand Up @@ -325,7 +365,12 @@ export class RejsonRlService {

const jsonSize = await this.estimateSize(client, keyName, jsonPath);

if (jsonSize > config.get('modules')['json']['sizeThreshold']) {
if (jsonSize > MODULES_CONFIG.json.sizeThreshold) {
// Additional check for cardinality
if (await this.isUnsafeBigJsonLength(client, keyName, jsonPath)) {
throw new BadRequestException(ERROR_MESSAGES.UNSAFE_BIG_JSON_LENGTH);
}

const type = await this.getJsonDataType(client, keyName, jsonPath);
result.downloaded = false;
result.type = type;
Expand Down
Loading