Skip to content

Commit c29ef76

Browse files
committed
fix: configurable query guards — max asc window + ES timeout
Adds two new config fields to ApiConfigs (+ Zod schema): - query_timeout: ES search timeout (default: '10s') - max_asc_window_days: max time range for sort=asc (default: 90) sort=asc now requires: 1. A valid 'after' or 'before' (ISO date or positive block number) 2. If ISO date (contains 'T'), must be within max_asc_window_days All get_actions queries (v1 + v2) now have a configurable ES timeout. Includes 17 unit tests for getSortDir validation.
1 parent ad33894 commit c29ef76

File tree

8 files changed

+250
-15
lines changed

8 files changed

+250
-15
lines changed

CHANGELOG.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,92 @@
1+
# Changelog
2+
3+
## 4.0.3 (2026-03-20)
4+
5+
### Security
6+
7+
* **Configurable Query Guards for `sort=asc`**: Prevents unbounded ascending sort queries on `get_actions` (v1 & v2) that could overload Elasticsearch by forcing full reverse segment scans across all shards.
8+
9+
### New Config Options
10+
11+
Two new optional fields in the `api` section of the chain config:
12+
13+
| Option | Type | Default | Description |
14+
|--------|------|---------|-------------|
15+
| `query_timeout` | `string` | `"10s"` | Elasticsearch search timeout per query |
16+
| `max_asc_window_days` | `number` | `90` | Maximum time range (in days) for `sort=asc` requests |
17+
18+
### Behavior Changes
19+
20+
* `sort=asc` on `get_actions` now **requires** a valid `after` or `before` parameter:
21+
* ISO date strings (must contain `T`, e.g., `2026-03-19T00:00:00Z`)
22+
* Positive integer block numbers (e.g., `425000000`)
23+
* ISO date `after` values must be within `max_asc_window_days` of the current time.
24+
* All `get_actions` queries (v1 + v2) now include a configurable Elasticsearch `timeout`.
25+
* `sort=desc` (default) is **unchanged** — no new restrictions apply.
26+
27+
### Testing
28+
29+
* Added 17 unit tests for `getSortDir` validation covering bounds checks, max window enforcement, block number acceptance, and garbage input rejection.
30+
31+
---
32+
33+
## 4.0.2 (2026-03-18)
34+
35+
### Testing
36+
37+
* **E2E High-Fidelity Test Framework**: 8-phase Docker-based test pipeline with port-isolated infrastructure, contract deployment, manifest-based load generation, and integrity checking.
38+
* **CI Workflow**: Target main branch, integrate unit tests into CI pipeline.
39+
40+
### Maintenance
41+
42+
* Updated all dependencies and fixed security vulnerabilities.
43+
44+
---
45+
46+
## 4.0.1 (2026-03-08)
47+
48+
### Fixes
49+
50+
* **Dynamic `global-agent` Loading**: Load `global-agent` dynamically via `createRequire` to resolve ESM import errors.
51+
52+
### Maintenance
53+
54+
* Upgraded all dependencies.
55+
* Added unit test suite (Bun-based) for API helpers, common functions, and config validation.
56+
57+
---
58+
59+
## 4.0.0-beta.5 (2025-11-04)
60+
61+
### Fixes
62+
63+
* **IndexerController**: Guard against missing chain config in `connect()` — reject and clear `connectionPromise` when config is not found.
64+
* **getFirstIndexedBlock**: Handle empty `cat.indices` results, add try/catch with error logging.
65+
66+
### Maintenance
67+
68+
* Dependency updates: `@elastic/elasticsearch` → 9.2.0, `commander` → 14.0.2, `ioredis` → 5.8.2, `typescript` → 5.9.3, `uWebSockets.js` → v20.55.0, `zod` → 4.1.12.
69+
70+
---
71+
72+
## 4.0.0-beta.4 (2025-09-30)
73+
74+
### Features
75+
76+
* **RabbitMQ Install Script**: Added `rabbit.sh` for automated RabbitMQ installation on Ubuntu.
77+
* **Explorer Oracle Metadata**: Explorer metadata route now includes oracle configuration.
78+
79+
### Fixes
80+
81+
* MongoDB client typing workaround for type compatibility.
82+
* Plugin loading cleanup — removed noisy error logs, improved type definitions.
83+
84+
### Maintenance
85+
86+
* Dependency updates: `@elastic/elasticsearch` → 9.1.1, `fastify` → 5.6.1, `mongodb` → 6.20.0, `typescript` → 5.9.2, `zod` → 4.1.11.
87+
88+
---
89+
190
# Changelog - Hyperion History API 4.0.0-beta.3 (pre-release)
291

392
This changelog summarizes the significant changes leading up to the `4.0.0-beta.3` release, based on Pull Request #157.

bun.lock

Lines changed: 22 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hyperion-history",
3-
"version": "4.0.2",
3+
"version": "4.0.3",
44
"description": "Scalable Full History API Solution for Antelope based blockchains",
55
"main": "./build/indexer/launcher.js",
66
"scripts": {

src/api/routes/v1-history/get_actions/get_actions.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,27 @@ async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
195195
}
196196
}
197197

198+
const maxAscWindowDays = fastify.manager.config.api.max_asc_window_days || 90;
199+
198200
if (reqBody.sort) {
199201
if (reqBody.sort === 'asc' || reqBody.sort === '1') {
202+
// sort=asc requires a valid, recent time range to prevent full-index reverse scans
203+
const after = reqBody.after;
204+
const before = reqBody.before;
205+
const isValidBound = (v) => v && (!isNaN(new Date(v).getTime()) || (Number.isInteger(Number(v)) && Number(v) > 0));
206+
if (!isValidBound(after) && !isValidBound(before)) {
207+
return {error: 'sort=asc requires a valid "after" or "before" (ISO date or block number) to bound the search'};
208+
}
209+
// validate the time window is not too wide (only for ISO date strings, not block numbers)
210+
if (typeof after === 'string' && after.includes('T')) {
211+
const afterDate = new Date(after);
212+
if (!isNaN(afterDate.getTime())) {
213+
const maxAge = Date.now() - (maxAscWindowDays * 86400000);
214+
if (afterDate.getTime() < maxAge) {
215+
return {error: `sort=asc "after" date must be within the last ${maxAscWindowDays} days`};
216+
}
217+
}
218+
}
200219
sort_direction = 'asc';
201220
} else if (reqBody.sort === 'desc' || reqBody.sort === '-1') {
202221
sort_direction = 'desc'
@@ -260,10 +279,12 @@ async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
260279

261280
const getActionsLimit = fastify.manager.config.api.limits.get_actions ?? 1000;
262281

282+
const queryTimeout = fastify.manager.config.api.query_timeout || '10s';
263283
const esOpts = {
264284
"index": fastify.manager.chain + '-action-*',
265285
"from": from || 0,
266286
"size": (size > getActionsLimit ? getActionsLimit : size),
287+
"timeout": queryTimeout,
267288
"query": queryStruct,
268289
"sort": {
269290
"global_sequence": sort_direction

src/api/routes/v2-history/get_actions/functions.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,27 @@ export function getSkipLimit(query: any, max?: number): { skip: number, limit: n
260260
return {skip, limit};
261261
}
262262

263-
export function getSortDir(query) {
263+
export function getSortDir(query, maxAscWindowDays = 90) {
264264
let sort_direction = 'desc';
265265
if (query.sort) {
266266
if (query.sort === 'asc' || query.sort === '1') {
267+
// sort=asc requires a valid, recent time range to prevent full-index reverse scans
268+
const after = query.after;
269+
const before = query.before;
270+
const isValidBound = (v) => v && (!isNaN(new Date(v).getTime()) || (Number.isInteger(Number(v)) && Number(v) > 0));
271+
if (!isValidBound(after) && !isValidBound(before)) {
272+
throw new Error('sort=asc requires a valid "after" or "before" (ISO date or block number) to bound the search');
273+
}
274+
// validate the time window is not too wide (only for ISO date strings, not block numbers)
275+
if (typeof after === 'string' && after.includes('T')) {
276+
const afterDate = new Date(after);
277+
if (!isNaN(afterDate.getTime())) {
278+
const maxAge = Date.now() - (maxAscWindowDays * 86400000);
279+
if (afterDate.getTime() < maxAge) {
280+
throw new Error(`sort=asc "after" date must be within the last ${maxAscWindowDays} days`);
281+
}
282+
}
283+
}
267284
sort_direction = 'asc';
268285
} else if (query.sort === 'desc' || query.sort === '-1') {
269286
sort_direction = 'desc'

src/api/routes/v2-history/get_actions/get_actions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
2323

2424
const {skip, limit} = getSkipLimit(query, maxActions);
2525

26-
const sort_direction = getSortDir(query);
26+
const maxAscWindowDays = fastify.manager.config.api.max_asc_window_days || 90;
27+
const sort_direction = getSortDir(query, maxAscWindowDays);
2728

2829
applyAccountFilters(query, queryStruct);
2930

@@ -52,10 +53,12 @@ async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
5253
indexPattern = fastify.manager.chain + '-action';
5354
}
5455

56+
const queryTimeout = fastify.manager.config.api.query_timeout || '10s';
5557
const esOpts = {
5658
"index": indexPattern,
5759
"from": skip || 0,
5860
"size": (limit > maxActions ? maxActions : limit) || 10,
61+
"timeout": queryTimeout,
5962
...query_body
6063
};
6164

src/interfaces/hyperionConfig.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ interface ApiConfigs {
166166
limits: ApiLimits;
167167
v1_chain_cache?: CachedRouteConfig[];
168168
explorer?: ExplorerConfigs;
169+
query_timeout?: string; // ES search timeout (e.g., "5s"), default: "10s"
170+
max_asc_window_days?: number; // max range in days for sort=asc queries, default: 90
169171
}
170172

171173
interface ExplorerConfigs {
@@ -320,6 +322,8 @@ export const HyperionApiConfigSchema = z.object({
320322
limits: ApiLimitsSchema,
321323
v1_chain_cache: z.array(CachedRouteConfigSchema).optional(),
322324
explorer: ExplorerConfigsSchema.optional(),
325+
query_timeout: z.string().optional(),
326+
max_asc_window_days: z.number().optional(),
323327
});
324328

325329
// Zod schema for tiered index allocation settings

0 commit comments

Comments
 (0)