fix(security): centralize safe frontmatter parsing

Move frontmatter parsing into server/shared/frontmatter.ts so every backend caller
uses the same gray-matter configuration instead of importing gray-matter directly.

The goal is to keep executable JS and JSON frontmatter engines disabled for
all markdown discovered from the filesystem, not only command routes.

Provider skills and shared skill metadata now go through parseFrontMatter too.
That closes the gap where plugin or provider markdown could regain default
gray-matter behavior simply because it lived outside the original command path.

Classify the new parser in backend boundaries so modules can depend on the
safe shared API without reaching into legacy utility paths.
This commit is contained in:
Haileyesus
2026-05-12 20:05:02 +03:00
parent aabf331e91
commit bacca8d62b
6 changed files with 24 additions and 17 deletions

View File

@@ -157,7 +157,11 @@ export default tseslint.config(
},
{
type: "backend-shared-utils", // shared backend runtime helpers that modules may import directly
pattern: ["server/shared/utils.{js,ts}", "server/shared/claude-cli-path.ts"], // classify the shared utils file so modules can depend on it explicitly
pattern: [
"server/shared/utils.{js,ts}",
"server/shared/frontmatter.ts",
"server/shared/claude-cli-path.ts",
], // classify shared utility files so modules can depend on them explicitly
mode: "file",
},
{

View File

@@ -2,9 +2,8 @@ import { readFile, readdir, stat } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import matter from 'gray-matter';
import { SkillsProvider } from '@/modules/providers/shared/skills/skills.provider.js';
import { parseFrontMatter } from '@/shared/frontmatter.js';
import type {
ProviderSkill,
ProviderSkillListOptions,
@@ -216,7 +215,7 @@ export class ClaudeSkillsProvider extends SkillsProvider {
commandPath: string,
): Promise<{ name: string; description: string }> {
const content = await readFile(commandPath, 'utf8');
const parsed = matter(content);
const parsed = parseFrontMatter(content);
const data = readObjectRecord(parsed.data) ?? {};
return {

View File

@@ -1,9 +1,11 @@
import express from 'express';
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import path from 'path';
import express from 'express';
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
import { parseFrontmatter } from '../utils/frontmatter.js';
import { parseFrontMatter } from '../shared/frontmatter.js';
import { findAppRoot, getModuleDir } from '../utils/runtime-paths.js';
const __dirname = getModuleDir(import.meta.url);
@@ -40,7 +42,7 @@ async function scanCommandsDirectory(dir, baseDir, namespace) {
// Parse markdown file for metadata
try {
const content = await fs.readFile(fullPath, 'utf8');
const { data: frontmatter, content: commandContent } = parseFrontmatter(content);
const { data: frontmatter, content: commandContent } = parseFrontMatter(content);
// Calculate relative path from baseDir for command name
const relativePath = path.relative(baseDir, fullPath);
@@ -513,7 +515,7 @@ router.post('/execute', async (req, res) => {
}
}
const content = await fs.readFile(commandPath, 'utf8');
const { data: metadata, content: commandContent } = parseFrontmatter(content);
const { data: metadata, content: commandContent } = parseFrontMatter(content);
// Basic argument replacement (will be enhanced in command parser utility)
let processedContent = commandContent;

View File

@@ -9,10 +9,10 @@ const frontmatterOptions = {
engines: {
js: disabledFrontmatterEngine,
javascript: disabledFrontmatterEngine,
json: disabledFrontmatterEngine
}
json: disabledFrontmatterEngine,
},
};
export function parseFrontmatter(content) {
export function parseFrontMatter(content: string) {
return matter(content, frontmatterOptions);
}

View File

@@ -15,9 +15,9 @@ import os from 'node:os';
import path from 'node:path';
import readline from 'node:readline';
import matter from 'gray-matter';
import type { NextFunction, Request, RequestHandler, Response } from 'express';
import { parseFrontMatter } from '@/shared/frontmatter.js';
import type {
AnyRecord,
ApiSuccessShape,
@@ -587,7 +587,7 @@ export async function readProviderSkillMarkdownDefinition(
skillPath: string,
): Promise<{ name: string; description: string }> {
const content = await readFile(skillPath, 'utf8');
const parsed = matter(content);
const parsed = parseFrontMatter(content);
const data = readObjectRecord(parsed.data) ?? {};
const fallbackName = path.basename(path.dirname(skillPath));

View File

@@ -1,9 +1,11 @@
import { execFile } from 'child_process';
import { promises as fs } from 'fs';
import path from 'path';
import { execFile } from 'child_process';
import { promisify } from 'util';
import { parse as parseShellCommand } from 'shell-quote';
import { parseFrontmatter } from './frontmatter.js';
import { parseFrontMatter } from '../shared/frontmatter.js';
const execFileAsync = promisify(execFile);
@@ -32,7 +34,7 @@ const BASH_COMMAND_ALLOWLIST = [
*/
export function parseCommand(content) {
try {
const parsed = parseFrontmatter(content);
const parsed = parseFrontMatter(content);
return {
data: parsed.data || {},
content: parsed.content || '',