Skip to content
Closed
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
3 changes: 2 additions & 1 deletion src/components/DecisionSupportLogicView.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import githubService from '../services/githubService';
import MDEditor from '@uiw/react-md-editor';
import { PageLayout, useDAKParams } from './framework';
import { sanitizeHtml } from '../utils/securityUtils';
import './DecisionSupportLogicView.css';

const DecisionSupportLogicView = () => {
Expand Down Expand Up @@ -965,7 +966,7 @@ define "Contraindication Present":
{selectedDialog.type === 'html' ? (
<div
className="html-content"
dangerouslySetInnerHTML={{ __html: selectedDialog.content }}
dangerouslySetInnerHTML={{ __html: sanitizeHtml(selectedDialog.content || '') }}
/>
) : (
<pre className="source-content">
Expand Down
33 changes: 18 additions & 15 deletions src/components/DocumentationViewer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { PageLayout } from './framework';
import { sanitizeHtml } from '../utils/securityUtils';
import './DocumentationViewer.css';

// Dynamically generate documentation files structure
Expand Down Expand Up @@ -91,7 +92,9 @@ const DocumentationViewer = () => {

const renderMarkdown = (markdown) => {
// Simple markdown to HTML conversion for basic formatting
let html = markdown;
// First sanitize the input markdown to prevent XSS in the source
const safeMd = sanitizeHtml(markdown);
let html = safeMd;

// Process tables first (before paragraph processing)
html = html.replace(/(\|[^\n]+\|\n\|[-\s|:]+\|\n(?:\|[^\n]+\|\n?)*)/gm, (match) => {
Expand All @@ -101,14 +104,14 @@ const DocumentationViewer = () => {

let tableHtml = '<table class="doc-table">\n<thead>\n<tr>\n';
headers.forEach(header => {
tableHtml += `<th>${header}</th>\n`;
tableHtml += `<th>${sanitizeHtml(header)}</th>\n`;
});
tableHtml += '</tr>\n</thead>\n<tbody>\n';

rows.forEach(row => {
tableHtml += '<tr>\n';
row.forEach(cell => {
tableHtml += `<td>${cell}</td>\n`;
tableHtml += `<td>${sanitizeHtml(cell)}</td>\n`;
});
tableHtml += '</tr>\n';
});
Expand All @@ -117,19 +120,19 @@ const DocumentationViewer = () => {
return tableHtml;
});

// Apply other markdown formatting
// Apply other markdown formatting with safe replacements
return html
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^#### (.*$)/gim, '<h4>$1</h4>')
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*)\*/gim, '<em>$1</em>')
.replace(/!\[([^\]]*)\]\(([^)]*)\)/gim, '<img alt="$1" src="$2" />')
.replace(/\[([^\]]*)\]\(([^)]*)\)/gim, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
.replace(/`([^`]*)`/gim, '<code>$1</code>')
.replace(/^- (.*$)/gim, '<li>$1</li>')
.replace(/^(\d+)\. (.*$)/gim, '<li>$2</li>')
.replace(/^# (.*$)/gim, (match, p1) => `<h1>${sanitizeHtml(p1)}</h1>`)
.replace(/^## (.*$)/gim, (match, p1) => `<h2>${sanitizeHtml(p1)}</h2>`)
.replace(/^### (.*$)/gim, (match, p1) => `<h3>${sanitizeHtml(p1)}</h3>`)
.replace(/^#### (.*$)/gim, (match, p1) => `<h4>${sanitizeHtml(p1)}</h4>`)
.replace(/\*\*(.*)\*\*/gim, (match, p1) => `<strong>${sanitizeHtml(p1)}</strong>`)
.replace(/\*(.*)\*/gim, (match, p1) => `<em>${sanitizeHtml(p1)}</em>`)
.replace(/!\[([^\]]*)\]\(([^)]*)\)/gim, (match, p1, p2) => `<img alt="${sanitizeHtml(p1)}" src="${sanitizeHtml(p2)}" />`)
.replace(/\[([^\]]*)\]\(([^)]*)\)/gim, (match, p1, p2) => `<a href="${sanitizeHtml(p2)}" target="_blank" rel="noopener noreferrer">${sanitizeHtml(p1)}</a>`)
.replace(/`([^`]*)`/gim, (match, p1) => `<code>${sanitizeHtml(p1)}</code>`)
.replace(/^- (.*$)/gim, (match, p1) => `<li>${sanitizeHtml(p1)}</li>`)
.replace(/^(\d+)\. (.*$)/gim, (match, p1, p2) => `<li>${sanitizeHtml(p2)}</li>`)
.replace(/\n\n/gim, '</p><p>')
.replace(/^\n/gim, '<p>')
.replace(/\n$/gim, '</p>');
Expand Down
47 changes: 36 additions & 11 deletions src/services/githubService.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Octokit } from '@octokit/rest';
import { processConcurrently } from '../utils/concurrency';
import repositoryCompatibilityCache from '../utils/repositoryCompatibilityCache';
import logger from '../utils/logger';
import { validateGitHubApiParams } from '../utils/securityUtils';

class GitHubService {
constructor() {
Expand Down Expand Up @@ -199,32 +200,44 @@ class GitHubService {

// Get specific organization data (public data, no auth required)
async getOrganization(orgLogin) {
// Validate input parameters
const validatedParams = validateGitHubApiParams({ orgLogin });
if (!validatedParams) {
throw new Error('Invalid organization login provided');
}

try {
// Create a temporary Octokit instance for public API calls if we don't have one
const octokit = this.octokit || new Octokit();

const { data } = await octokit.rest.orgs.get({
org: orgLogin
org: validatedParams.orgLogin
});
return data;
} catch (error) {
console.error(`Failed to fetch organization ${orgLogin}:`, error);
console.error(`Failed to fetch organization ${validatedParams.orgLogin}:`, error);
throw error;
}
}

// Get specific user data (public data, no auth required)
async getUser(username) {
// Validate input parameters
const validatedParams = validateGitHubApiParams({ username });
if (!validatedParams) {
throw new Error('Invalid username provided');
}

try {
// Create a temporary Octokit instance for public API calls if we don't have one
const octokit = this.octokit || new Octokit();

const { data } = await octokit.rest.users.getByUsername({
username
username: validatedParams.username
});
return data;
} catch (error) {
console.error(`Failed to fetch user ${username}:`, error);
console.error(`Failed to fetch user ${validatedParams.username}:`, error);
throw error;
}
}
Expand Down Expand Up @@ -604,13 +617,19 @@ class GitHubService {

// Get a specific repository
async getRepository(owner, repo) {
// Validate input parameters
const validatedParams = validateGitHubApiParams({ owner, repo });
if (!validatedParams) {
throw new Error('Invalid repository parameters provided');
}

try {
// Use authenticated octokit if available, otherwise create a public instance for public repos
const octokit = this.isAuth() ? this.octokit : new Octokit();

const { data } = await octokit.rest.repos.get({
owner,
repo,
owner: validatedParams.owner,
repo: validatedParams.repo,
});
return data;
} catch (error) {
Expand All @@ -621,17 +640,23 @@ class GitHubService {

// Get repository branches
async getBranches(owner, repo) {
// Validate input parameters
const validatedParams = validateGitHubApiParams({ owner, repo });
if (!validatedParams) {
throw new Error('Invalid repository parameters provided');
}

try {
console.log(`githubService.getBranches: Fetching branches for ${owner}/${repo}`);
console.log(`githubService.getBranches: Fetching branches for ${validatedParams.owner}/${validatedParams.repo}`);
console.log('githubService.getBranches: Authentication status:', this.isAuth());

// Use authenticated octokit if available, otherwise create a public instance for public repos
const octokit = this.isAuth() ? this.octokit : new Octokit();
console.log('githubService.getBranches: Using', this.isAuth() ? 'authenticated' : 'public', 'octokit instance');

const { data } = await octokit.rest.repos.listBranches({
owner,
repo,
owner: validatedParams.owner,
repo: validatedParams.repo,
per_page: 100
});

Expand All @@ -642,8 +667,8 @@ class GitHubService {
console.error('githubService.getBranches: Error details:', {
status: error.status,
message: error.message,
owner,
repo
owner: validatedParams.owner,
repo: validatedParams.repo
});
throw error;
}
Expand Down
27 changes: 27 additions & 0 deletions src/utils/navigationUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Utility functions for handling navigation with command-click support
*/

import { validateUrlScheme } from './securityUtils';

/**
* Detects if a click event should open in a new tab
* @param {MouseEvent} event - The click event
Expand All @@ -17,6 +19,17 @@ export const shouldOpenInNewTab = (event) => {
* @returns {string} - The full URL
*/
export const constructFullUrl = (relativePath) => {
// Validate the relative path to prevent XSS
if (!relativePath || typeof relativePath !== 'string') {
return window.location.origin;
}

// Basic validation - don't allow dangerous protocols
if (relativePath.includes('javascript:') || relativePath.includes('data:')) { // eslint-disable-line no-script-url
console.warn('Blocked dangerous URL scheme in path:', relativePath);
return window.location.origin;
}

const basePath = process.env.PUBLIC_URL || '';
const baseUrl = window.location.origin;

Expand All @@ -42,6 +55,20 @@ export const constructFullUrl = (relativePath) => {
* @param {Object} state - Optional state to pass with navigation
*/
export const handleNavigationClick = (event, path, navigate, state = null) => {
// Validate the path to prevent malicious navigation
if (!path || typeof path !== 'string') {
console.warn('Invalid navigation path provided');
return;
}

// For external URLs, validate the scheme
if (path.includes('://')) {
if (!validateUrlScheme(path)) {
console.warn('Blocked navigation to potentially dangerous URL:', path);
return;
}
}

if (shouldOpenInNewTab(event)) {
// Open in new tab
const fullUrl = constructFullUrl(path);
Expand Down
Loading