Skip to content

Commit 8c880ec

Browse files
author
Dev Agent Amelia
committed
fix: v0.1.3 — server startup crash + security hardening
- Fix server startup crash when installed via npx (package.json path) - [HIGH] Validate entitySetName with safe identifier regex across all tools (F-01) - [MEDIUM] Validate relationshipName/relatedEntitySetName in relation tools (F-10) - [LOW] Consolidate inline OData escaping to centralized esc() utility (F-06) - Add validation.utils.ts with shared Zod schemas for input sanitization
1 parent e3d72a7 commit 8c880ec

17 files changed

Lines changed: 73 additions & 32 deletions

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) — [Semantic V
1717

1818
---
1919

20+
## [0.1.3] — 2025-06-22
21+
22+
### Fixed
23+
- Server startup crash when installed via `npx` — incorrect `package.json` path resolution from `dist/` (was `../../package.json`, now `../package.json`)
24+
25+
### Security
26+
- **[HIGH]** `entitySetName` now validated against a safe identifier regex (`/^[a-zA-Z_][a-zA-Z0-9_]*$/`) across all tools — prevents path traversal within same origin (F-01)
27+
- **[MEDIUM]** `relationshipName` and `relatedEntitySetName` now validated with the same safe identifier regex in relation tools (F-10)
28+
- Consolidated all inline OData single-quote escaping calls to use the centralized `esc()` utility for consistency (F-06)
29+
30+
---
31+
2032
## [0.1.0] — 2025-04-01
2133

2234
### Added

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mcp-dataverse",
3-
"version": "0.1.0",
3+
"version": "0.1.3",
44
"description": "MCP Server for Microsoft Dataverse Web API",
55
"type": "module",
66
"main": "dist/server.js",

server.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
"url": "https://github.com/codeurali/mcp-dataverse",
88
"source": "github"
99
},
10-
"version": "0.1.0",
10+
"version": "0.1.3",
1111
"packages": [
1212
{
1313
"registryType": "npm",
1414
"identifier": "mcp-dataverse",
15-
"version": "0.1.0",
15+
"version": "0.1.3",
1616
"transport": {
1717
"type": "stdio"
1818
},

src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ const TEAM_TOOL_NAMES = new Set(teamTools.map(t => t.name));
8787

8888
// Read version from package.json so server.ts never drifts out of sync
8989
const __dirname = dirname(fileURLToPath(import.meta.url));
90-
const SERVER_VERSION = (JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf-8')) as { version: string }).version;
90+
const SERVER_VERSION = (JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8')) as { version: string }).version;
9191

9292
/**
9393
* Routes a tool call to its handler. Used directly by the request handler

src/tools/actions.tools.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from 'zod';
22
import type { DataverseAdvancedClient } from '../dataverse/dataverse-client-advanced.js';
3+
import { safeEntitySetName } from './validation.utils.js';
34

45
export const actionTools = [
56
{
@@ -120,7 +121,7 @@ const ExecuteFunctionInput = z.object({
120121
});
121122

122123
const ExecuteBoundActionInput = z.object({
123-
entitySetName: z.string().min(1),
124+
entitySetName: safeEntitySetName,
124125
id: z.string().uuid(),
125126
actionName: z
126127
.string()
@@ -142,7 +143,7 @@ const ListTableDependenciesInput = z.object({
142143
});
143144

144145
const ExecuteBoundFunctionInput = z.object({
145-
entitySetName: z.string().min(1),
146+
entitySetName: safeEntitySetName,
146147
id: z.string().uuid(),
147148
functionName: z
148149
.string()

src/tools/annotations.tools.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { z } from 'zod';
22
import type { DataverseAdvancedClient } from '../dataverse/dataverse-client-advanced.js';
3+
import { esc } from '../dataverse/dataverse-client.utils.js';
4+
import { safeEntitySetName } from './validation.utils.js';
35

46
const GetAnnotationsInput = z.object({
57
recordId: z.string().uuid(),
@@ -10,7 +12,7 @@ const GetAnnotationsInput = z.object({
1012

1113
const CreateAnnotationInput = z.object({
1214
recordId: z.string().uuid(),
13-
entitySetName: z.string().min(1),
15+
entitySetName: safeEntitySetName,
1416
notetext: z.string().optional(),
1517
subject: z.string().optional(),
1618
filename: z.string().optional(),
@@ -121,7 +123,7 @@ export async function handleAnnotationTool(
121123
const filterParts = [`_objectid_value eq ${params.recordId}`];
122124
if (params.mimeTypeFilter) {
123125
filterParts.push(
124-
`mimetype eq '${params.mimeTypeFilter.replace(/'/g, "''")}'`,
126+
`mimetype eq '${esc(params.mimeTypeFilter)}'`,
125127
);
126128
}
127129

src/tools/audit.tools.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from 'zod';
22
import type { DataverseAdvancedClient } from '../dataverse/dataverse-client-advanced.js';
3+
import { esc } from '../dataverse/dataverse-client.utils.js';
34

45
const AUDIT_ACTION_NAMES: Record<number, string> = {
56
1: 'Create',
@@ -135,7 +136,7 @@ export async function handleAuditTool(
135136
filters.push(`_objectid_value eq ${params.recordId}`);
136137
}
137138
if (params.entityLogicalName) {
138-
const escaped = params.entityLogicalName.replace(/'/g, "''");
139+
const escaped = esc(params.entityLogicalName);
139140
filters.push(`objecttypecode eq '${escaped}'`);
140141
}
141142
if (params.userId) {

src/tools/crud.tools.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from 'zod';
22
import type { DataverseClient } from '../dataverse/dataverse-client.js';
33
import { esc } from '../dataverse/dataverse-client.utils.js';
4+
import { safeEntitySetName } from './validation.utils.js';
45

56
export const crudTools = [
67
{
@@ -109,31 +110,31 @@ export const crudTools = [
109110
];
110111

111112
const GetInput = z.object({
112-
entitySetName: z.string().min(1),
113+
entitySetName: safeEntitySetName,
113114
id: z.string().uuid(),
114115
select: z.array(z.string()).optional(),
115116
});
116117

117118
const CreateInput = z.object({
118-
entitySetName: z.string().min(1),
119+
entitySetName: safeEntitySetName,
119120
data: z.record(z.unknown()),
120121
});
121122

122123
const UpdateInput = z.object({
123-
entitySetName: z.string().min(1),
124+
entitySetName: safeEntitySetName,
124125
id: z.string().uuid(),
125126
data: z.record(z.unknown()),
126127
etag: z.string().optional(),
127128
});
128129

129130
const DeleteInput = z.object({
130-
entitySetName: z.string().min(1),
131+
entitySetName: safeEntitySetName,
131132
id: z.string().uuid(),
132133
confirm: z.boolean(),
133134
});
134135

135136
const UpsertInput = z.object({
136-
entitySetName: z.string().min(1),
137+
entitySetName: safeEntitySetName,
137138
alternateKey: z.string().min(1).optional(),
138139
alternateKeyValue: z.string().min(1).optional(),
139140
alternateKeys: z.record(z.string()).optional(),
@@ -146,7 +147,7 @@ const UpsertInput = z.object({
146147
);
147148

148149
const AssignInput = z.object({
149-
entitySetName: z.string().min(1),
150+
entitySetName: safeEntitySetName,
150151
id: z.string().uuid(),
151152
ownerType: z.enum(['systemuser', 'team']),
152153
ownerId: z.string().uuid(),

src/tools/customization.tools.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from 'zod';
22
import type { DataverseAdvancedClient } from '../dataverse/dataverse-client-advanced.js';
3+
import { esc } from '../dataverse/dataverse-client.utils.js';
34

45
const STAGE_NAMES: Record<number, string> = {
56
10: 'Pre-validation',
@@ -118,7 +119,7 @@ export async function handleCustomizationTool(
118119

119120
const filters: string[] = ['isprivate eq false'];
120121
if (nameFilter) {
121-
filters.push(`contains(name,'${nameFilter.replace(/'/g, "''")}')`);
122+
filters.push(`contains(name,'${esc(nameFilter)}')`);
122123
}
123124

124125
const result = await client.query<SdkMessage>('sdkmessages', {

src/tools/query.tools.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from 'zod';
22
import type { DataverseAdvancedClient } from '../dataverse/dataverse-client-advanced.js';
3+
import { safeEntitySetName } from './validation.utils.js';
34

45
/**
56
* Dataverse entities whose EntitySetName does not follow the simple <logicalName>+s pattern.
@@ -105,7 +106,7 @@ export const queryTools = [
105106
];
106107

107108
const QueryInput = z.object({
108-
entitySetName: z.string().min(1),
109+
entitySetName: safeEntitySetName,
109110
select: z.array(z.string()).optional(),
110111
filter: z.string().optional(),
111112
orderby: z.string().optional(),
@@ -121,7 +122,7 @@ const FetchXmlInput = z.object({
121122
});
122123

123124
const RetrieveWithPagingInput = z.object({
124-
entitySetName: z.string().min(1),
125+
entitySetName: safeEntitySetName,
125126
select: z.array(z.string()).optional(),
126127
filter: z.string().optional(),
127128
orderby: z.string().optional(),

0 commit comments

Comments
 (0)