Skip to content

Conversation

@TabishB
Copy link
Contributor

@TabishB TabishB commented Jan 28, 2026

Summary

Implement a new openspec dashboard command that launches a web-based dashboard for browsing OpenSpec project data. The dashboard runs on localhost with zero external dependencies and provides a single-page interface for viewing active changes with progress tracking, specs organized by domain, archived changes, and rendered markdown content for any artifact.

Key Features

  • Local HTTP server with JSON API endpoints
  • Self-contained HTML dashboard with inline CSS/JS
  • Port selection with automatic fallback (3000-3010)
  • Cross-platform browser opening
  • Graceful shutdown on Ctrl+C
  • Client-side markdown rendering

Testing

All 1190 existing tests pass. New functionality verified to compile and integrate correctly with the CLI.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added openspec dashboard command that launches a local web-based interface for browsing specs, changes, and archived items.
    • Displays artifact content with markdown rendering in an embedded dashboard UI.
    • Configurable port (default 3000) and browser auto-open behavior via --port and --no-open options.

✏️ Tip: You can customize this high-level summary in your review settings.

Implement a new `openspec dashboard` command that starts a local HTTP server and opens a browser-based dashboard for browsing OpenSpec project data. The dashboard provides a single-page app with zero external dependencies, serving project metrics, active/draft/completed changes with progress tracking, specs organized by domain, archived changes, and rendered markdown content for any artifact.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

A new web-based dashboard feature is introduced to openspec. It includes design documentation, formal specifications, implementation tasks, and the actual code implementation of a DashboardCommand that serves a local HTTP server with API endpoints for browsing specs, changes, and archives through an embedded HTML dashboard.

Changes

Cohort / File(s) Summary
Documentation and Specifications
openspec/changes/add-web-dashboard-command/design.md, openspec/changes/add-web-dashboard-command/proposal.md, openspec/changes/add-web-dashboard-command/specs/cli-dashboard/spec.md, openspec/changes/add-web-dashboard-command/tasks.md
Documents outlining the web dashboard design (Node.js HTTP server, browser launch, markdown rendering), feature proposal with JSON API and embedded assets, formal specification with command lifecycle and security requirements, and detailed implementation tasks including API endpoints, port handling, and UI behavior.
Metadata
openspec/changes/add-web-dashboard-command/.openspec.yaml
Metadata file defining schema as spec-driven and creation timestamp.
Dashboard Implementation
src/core/dashboard.ts
New DashboardCommand class (748 lines) implementing HTTP server with /api/changes, /api/specs, /api/archive, /api/artifact endpoints; embedded HTML dashboard with inline CSS/JS; port selection with fallback; browser launching; path traversal validation; graceful shutdown.
CLI Integration
src/cli/index.ts
Registers new "dashboard" CLI command with --port and --no-open options; invokes DashboardCommand.execute() with error handling.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI as CLI Layer
    participant DashboardCmd as DashboardCommand
    participant Server as HTTP Server
    participant FileSystem as File System
    participant Browser as Browser

    User->>CLI: openspec dashboard [--port N] [--no-open]
    CLI->>DashboardCmd: execute(targetPath, options)
    DashboardCmd->>FileSystem: Locate openspec directory
    DashboardCmd->>DashboardCmd: findAvailablePort(3000, ...)
    DashboardCmd->>Server: Create & listen on available port
    Server-->>DashboardCmd: Server ready on port P
    alt --no-open flag not set
        DashboardCmd->>Browser: openBrowser(http://localhost:P)
    end
    
    Browser->>Server: GET / (request dashboard HTML)
    Server-->>Browser: getDashboardHtml() with embedded CSS/JS
    
    Browser->>Server: GET /api/changes
    Server->>FileSystem: Read changes directory
    FileSystem-->>Server: Changes data with artifact status
    Server-->>Browser: JSON response
    
    Browser->>Browser: Render dashboard with sidebar
    Browser->>Server: GET /api/artifact?type=change&name=X&file=proposal.md
    Server->>FileSystem: Read artifact file with path validation
    FileSystem-->>Server: Artifact content
    Server-->>Browser: Artifact markdown
    
    Browser->>Browser: Render markdown content
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A dashboard blooms with port-bound grace,
HTML whispers secrets to a chosen place,
APIs dance with specs and changes dear,
No CDN chains—just Node.js here! 🚀
Markdown rendered in browser's care,
The openspec dashboard floats through air ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title mentions 'openspec dashboard command' but includes unclear bracketed context '[With Old OpenSpec Version]' that obscures the main change and reduces clarity. Simplify the title to 'Add openspec dashboard command' by removing the bracketed qualifier, making it clear and concise for teammates scanning history.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Jan 28, 2026

Greptile Overview

Greptile Summary

This PR adds a new openspec dashboard command that launches a local web server with a browser-based UI for viewing OpenSpec project data. The implementation reuses existing data-gathering utilities and serves a self-contained HTML page with inline CSS/JavaScript.

Key changes:

  • Added DashboardCommand class in src/core/dashboard.ts with HTTP server, JSON API routes (/api/changes, /api/specs, /api/archive, /api/artifact), and embedded HTML/CSS/JS
  • Registered dashboard command in CLI with --port and --no-open options
  • Path traversal prevention implemented for artifact requests
  • Client-side markdown rendering with minimal parser
  • Port auto-fallback (3000-3010) when default unavailable
  • Cross-platform browser opening via child_process.exec

Issues found:

  • XSS vulnerabilities in inline HTML generation where c.status is not escaped before being used in class names and onclick attributes (lines 495, 516, 529)
  • Signal handlers (SIGINT/SIGTERM) registered but never cleaned up, causing potential memory leaks if execute() called multiple times
  • Markdown regex patterns for bold/italic have edge cases that could lead to incorrect parsing

Confidence Score: 3/5

  • This PR has security issues that need addressing before merge
  • While the implementation is well-structured and follows project patterns, there are critical XSS vulnerabilities in the HTML generation code where user-controlled data (change names, statuses, spec names) is concatenated into onclick attributes without proper escaping. The signal handler cleanup issue could cause memory leaks in certain scenarios.
  • Pay close attention to src/core/dashboard.ts for XSS vulnerabilities and signal handler cleanup

Important Files Changed

Filename Overview
src/core/dashboard.ts New dashboard command implementation with HTTP server, JSON API endpoints, and embedded HTML/CSS/JS for web-based project viewing
src/cli/index.ts Added dashboard command registration with --port and --no-open options following existing CLI patterns

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI
    participant DashboardCommand
    participant HTTPServer
    participant Browser
    participant FileSystem

    User->>CLI: openspec dashboard --port 3000
    CLI->>DashboardCommand: execute('.', {port: 3000, open: true})
    DashboardCommand->>FileSystem: Check openspec/ exists
    FileSystem-->>DashboardCommand: Directory exists
    DashboardCommand->>DashboardCommand: findAvailablePort(3000)
    DashboardCommand->>HTTPServer: Create server & listen on port 3000
    HTTPServer-->>DashboardCommand: Server started
    DashboardCommand->>Browser: Open http://localhost:3000
    DashboardCommand->>DashboardCommand: Register SIGINT/SIGTERM handlers
    Browser->>HTTPServer: GET /
    HTTPServer->>DashboardCommand: handleRequest()
    DashboardCommand-->>Browser: Return HTML page (getDashboardHtml())
    Browser->>HTTPServer: GET /api/changes
    HTTPServer->>DashboardCommand: handleChanges()
    DashboardCommand->>FileSystem: Read openspec/changes/
    DashboardCommand->>FileSystem: Check artifacts exist
    DashboardCommand-->>Browser: JSON {draft, active, completed}
    Browser->>HTTPServer: GET /api/specs
    HTTPServer->>DashboardCommand: handleSpecs()
    DashboardCommand->>FileSystem: Read openspec/specs/
    DashboardCommand->>FileSystem: Parse spec.md files
    DashboardCommand-->>Browser: JSON [specs with domains]
    Browser->>HTTPServer: GET /api/artifact?type=change&name=foo&file=proposal.md
    HTTPServer->>DashboardCommand: handleArtifact()
    DashboardCommand->>DashboardCommand: Validate path (prevent traversal)
    DashboardCommand->>FileSystem: Read artifact file
    DashboardCommand-->>Browser: JSON {content: "..."}
    Browser->>Browser: renderMarkdown(content)
    User->>CLI: Ctrl+C (SIGINT)
    DashboardCommand->>HTTPServer: Close server
    DashboardCommand->>CLI: process.exit(0)
Loading

@vibe-kanban-cloud
Copy link

Review Complete

Your review story is ready!

View Story

Comment !reviewfast on this PR to re-generate the story.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

} else if (c.status === 'completed') {
extra = '<span class="progress-mini">Done</span>';
}
html += '<div class="sidebar-item' + cls + '" onclick="selectChange(\\''+esc(c.name)+'\\',\\''+c.status+'\\')"><span class="dot dot-'+c.status+'"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(c.name)+'</span>'+extra+'</div>';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

concatenated inline HTML strings with user-controlled data through string interpolation on line 495, creating XSS vulnerability

Suggested change
html += '<div class="sidebar-item' + cls + '" onclick="selectChange(\\''+esc(c.name)+'\\',\\''+c.status+'\\')"><span class="dot dot-'+c.status+'"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(c.name)+'</span>'+extra+'</div>';
html += '<div class="sidebar-item' + cls + '" onclick="selectChange(\''+esc(c.name)+'\',\''+esc(c.status)+'\')"><span class="dot dot-'+esc(c.status)+'"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(c.name)+'</span>'+extra+'</div>';
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/dashboard.ts
Line: 495:495

Comment:
concatenated inline HTML strings with user-controlled data through string interpolation on line 495, creating XSS vulnerability

```suggestion
        html += '<div class="sidebar-item' + cls + '" onclick="selectChange(\''+esc(c.name)+'\',\''+esc(c.status)+'\')"><span class="dot dot-'+esc(c.status)+'"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(c.name)+'</span>'+extra+'</div>';
```

How can I resolve this? If you propose a fix, please make it concise.

// Code (backtick) - must come before bold/italic to avoid conflicts
s = s.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
// Bold
s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regex /\*\*([^*]+)\*\*/g for bold text doesn't handle nested bold or multiple asterisks correctly (e.g. **bold*** would match incorrectly)

Suggested change
s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/dashboard.ts
Line: 729:729

Comment:
regex `/\*\*([^*]+)\*\*/g` for bold text doesn't handle nested bold or multiple asterisks correctly (e.g. `**bold***` would match incorrectly)

```suggestion
    s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
```

How can I resolve this? If you propose a fix, please make it concise.

// Bold
s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
// Italic
s = s.replace(/\\*([^*]+)\\*/g, '<em>$1</em>');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regex /\*([^*]+)\*/g for italic incorrectly captures bold markers (e.g. **bold** would partially match)

Suggested change
s = s.replace(/\\*([^*]+)\\*/g, '<em>$1</em>');
s = s.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>');
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/dashboard.ts
Line: 731:731

Comment:
regex `/\*([^*]+)\*/g` for italic incorrectly captures bold markers (e.g. `**bold**` would partially match)

```suggestion
    s = s.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>');
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +55 to +56
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signal handlers registered but never cleaned up, can cause memory leaks or duplicate handlers if execute() called multiple times

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/dashboard.ts
Line: 55:56

Comment:
signal handlers registered but never cleaned up, can cause memory leaks or duplicate handlers if `execute()` called multiple times

How can I resolve this? If you propose a fix, please make it concise.

const key = 'spec:' + s.name;
const cls = activeItem === key ? ' active' : '';
const reqLabel = s.requirementCount === 1 ? 'req' : 'reqs';
html += '<div class="sidebar-item' + cls + '" onclick="selectSpec(\\''+esc(s.name)+'\\')"><span class="dot dot-spec"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(s.name)+'</span><span class="progress-mini">'+s.requirementCount+' '+reqLabel+'</span></div>';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inline attribute onclick with string interpolation on line 516 creates XSS risk if spec name contains quotes

Suggested change
html += '<div class="sidebar-item' + cls + '" onclick="selectSpec(\\''+esc(s.name)+'\\')"><span class="dot dot-spec"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(s.name)+'</span><span class="progress-mini">'+s.requirementCount+' '+reqLabel+'</span></div>';
html += '<div class="sidebar-item' + cls + '" onclick="selectSpec(\''+esc(s.name)+'\')"><span class="dot dot-spec"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(s.name)+'</span><span class="progress-mini">'+s.requirementCount+' '+reqLabel+'</span></div>';
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/dashboard.ts
Line: 516:516

Comment:
inline attribute `onclick` with string interpolation on line 516 creates XSS risk if spec name contains quotes

```suggestion
          html += '<div class="sidebar-item' + cls + '" onclick="selectSpec(\''+esc(s.name)+'\')"><span class="dot dot-spec"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(s.name)+'</span><span class="progress-mini">'+s.requirementCount+' '+reqLabel+'</span></div>';
```

How can I resolve this? If you propose a fix, please make it concise.

for (const name of archiveData) {
const key = 'archive:' + name;
const cls = activeItem === key ? ' active' : '';
html += '<div class="sidebar-item' + cls + '" onclick="selectArchive(\\''+esc(name)+'\\')"><span class="dot dot-archive"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(name)+'</span></div>';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inline attribute onclick with string interpolation creates XSS risk

Suggested change
html += '<div class="sidebar-item' + cls + '" onclick="selectArchive(\\''+esc(name)+'\\')"><span class="dot dot-archive"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(name)+'</span></div>';
html += '<div class="sidebar-item' + cls + '" onclick="selectArchive(\''+esc(name)+'\')"><span class="dot dot-archive"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(name)+'</span></div>';
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/dashboard.ts
Line: 529:529

Comment:
inline attribute `onclick` with string interpolation creates XSS risk

```suggestion
        html += '<div class="sidebar-item' + cls + '" onclick="selectArchive(\''+esc(name)+'\')"><span class="dot dot-archive"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(name)+'</span></div>';
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/cli/index.ts`:
- Around line 205-216: Validate the --port option before calling
DashboardCommand.execute: in the dashboard command action, if options?.port is
provided, parse it with parseInt and check Number.isFinite and that it's an
integer within the valid TCP range (1–65535); if parsing fails or the value is
out of range, throw or exit with a clear error message (e.g., "Invalid port:
must be an integer between 1 and 65535") instead of passing NaN or an
out‑of‑range value to DashboardCommand.execute; otherwise pass undefined when no
port was provided. Ensure you update the argument passed to
DashboardCommand.execute (the object with port and open) to use the validated
numeric port variable.

In `@src/core/dashboard.ts`:
- Around line 329-334: The sendJson method currently sets a wildcard CORS header
which exposes local project data; remove the 'Access-Control-Allow-Origin': '*'
entry from the res.writeHead call in sendJson (or replace it with a strict
allowlist check using the incoming request Origin and a configured allowed
origin) so the API remains same‑origin only; update any callers to provide an
Origin or config value if you implement allowlist behavior and ensure sendJson
only emits the header when the Origin matches an approved value.
- Around line 724-739: The link rendering in inline() allows unsafe schemes and
omits rel attributes; update the link replacement to use a callback that (1)
URL-decodes/validates the captured href against a whitelist of safe
schemes/paths (allow only http:, https:, mailto:, fragment '#' and relative
paths like starting with '/', './', '../' or no scheme), (2) sanitize/escape the
href via escHtml before inserting it, and (3) output the anchor with
target="_blank" plus rel="noopener noreferrer" when allowed; if the href is not
allowed, render only the link text without a clickable href (or use a safe
fallback like href="#" without target/rel). Implement this logic inside inline()
where the /\[([^\]]+)\]\(([^)]+)\)/g replacement is done and reuse escHtml for
escaping.

Comment on lines +205 to +216
program
.command('dashboard')
.description('Open a web-based dashboard for browsing specs, changes, and archive')
.option('--port <number>', 'Port to run the server on (default: 3000)')
.option('--no-open', 'Do not open the browser automatically')
.action(async (options?: { port?: string; open?: boolean }) => {
try {
const dashboardCommand = new DashboardCommand();
await dashboardCommand.execute('.', {
port: options?.port ? parseInt(options.port, 10) : undefined,
open: options?.open,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate --port before passing it through.
parseInt can yield NaN or out‑of‑range values, which then reach the server and cause runtime errors. Fail fast with a clear message.

🔧 Proposed fix
   .action(async (options?: { port?: string; open?: boolean }) => {
     try {
       const dashboardCommand = new DashboardCommand();
+      const parsedPort =
+        options?.port !== undefined ? Number(options.port) : undefined;
+      if (
+        options?.port !== undefined &&
+        (!Number.isInteger(parsedPort) || parsedPort! < 1 || parsedPort! > 65535)
+      ) {
+        throw new Error(`Invalid port "${options.port}". Use an integer between 1 and 65535.`);
+      }
       await dashboardCommand.execute('.', {
-        port: options?.port ? parseInt(options.port, 10) : undefined,
+        port: parsedPort,
         open: options?.open,
       });
     } catch (error) {
🤖 Prompt for AI Agents
In `@src/cli/index.ts` around lines 205 - 216, Validate the --port option before
calling DashboardCommand.execute: in the dashboard command action, if
options?.port is provided, parse it with parseInt and check Number.isFinite and
that it's an integer within the valid TCP range (1–65535); if parsing fails or
the value is out of range, throw or exit with a clear error message (e.g.,
"Invalid port: must be an integer between 1 and 65535") instead of passing NaN
or an out‑of‑range value to DashboardCommand.execute; otherwise pass undefined
when no port was provided. Ensure you update the argument passed to
DashboardCommand.execute (the object with port and open) to use the validated
numeric port variable.

Comment on lines +329 to +334
private sendJson(res: http.ServerResponse, status: number, data: any): void {
res.writeHead(status, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
});
res.end(JSON.stringify(data));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove wildcard CORS to prevent cross‑origin reads of local data.
The API is same‑origin with the embedded dashboard, so Access-Control-Allow-Origin: * allows any website to read local project data while the server is running. Remove it (or strictly restrict it).

🔧 Proposed fix
   res.writeHead(status, {
     'Content-Type': 'application/json',
-    'Access-Control-Allow-Origin': '*',
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private sendJson(res: http.ServerResponse, status: number, data: any): void {
res.writeHead(status, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
});
res.end(JSON.stringify(data));
private sendJson(res: http.ServerResponse, status: number, data: any): void {
res.writeHead(status, {
'Content-Type': 'application/json',
});
res.end(JSON.stringify(data));
🤖 Prompt for AI Agents
In `@src/core/dashboard.ts` around lines 329 - 334, The sendJson method currently
sets a wildcard CORS header which exposes local project data; remove the
'Access-Control-Allow-Origin': '*' entry from the res.writeHead call in sendJson
(or replace it with a strict allowlist check using the incoming request Origin
and a configured allowed origin) so the API remains same‑origin only; update any
callers to provide an Origin or config value if you implement allowlist behavior
and ensure sendJson only emits the header when the Origin matches an approved
value.

Comment on lines +724 to +739
function inline(text) {
let s = escHtml(text);
// Code (backtick) - must come before bold/italic to avoid conflicts
s = s.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
// Bold
s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
// Italic
s = s.replace(/\\*([^*]+)\\*/g, '<em>$1</em>');
// Links
s = s.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
return s;
}

function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden rendered links (tabnabbing + unsafe schemes).
Links open in a new tab without rel="noopener noreferrer" and accept any scheme. That allows javascript: links from markdown and tabnabbing. Add rel and filter schemes to http(s)/mailto/relative/fragment only.

🔧 Proposed fix
   function inline(text) {
     let s = escHtml(text);
     // Code (backtick) - must come before bold/italic to avoid conflicts
     s = s.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
     // Bold
     s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
     // Italic
     s = s.replace(/\*([^*]+)\*/g, '<em>$1</em>');
     // Links
-    s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
+    s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (m, text, url) => {
+      const safeUrl = sanitizeUrl(url);
+      return '<a href="' + safeUrl + '" target="_blank" rel="noopener noreferrer">' + text + '</a>';
+    });
     return s;
   }
 
+  function sanitizeUrl(url) {
+    const u = url.trim();
+    if (/^(https?:|mailto:|\/|#|\.\.?\/)/i.test(u)) return u;
+    return '#';
+  }
+
   function escHtml(s) {
     return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
   }
🤖 Prompt for AI Agents
In `@src/core/dashboard.ts` around lines 724 - 739, The link rendering in inline()
allows unsafe schemes and omits rel attributes; update the link replacement to
use a callback that (1) URL-decodes/validates the captured href against a
whitelist of safe schemes/paths (allow only http:, https:, mailto:, fragment '#'
and relative paths like starting with '/', './', '../' or no scheme), (2)
sanitize/escape the href via escHtml before inserting it, and (3) output the
anchor with target="_blank" plus rel="noopener noreferrer" when allowed; if the
href is not allowed, render only the link text without a clickable href (or use
a safe fallback like href="#" without target/rel). Implement this logic inside
inline() where the /\[([^\]]+)\]\(([^)]+)\)/g replacement is done and reuse
escHtml for escaping.

@TabishB TabishB changed the title Add openspec dashboard command [With Old OpenSpec Version] Add openspec dashboard command Jan 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants