mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
feat(security): escape HTML attributes and content in model modal, fixes #720
- Import `escapeAttribute` and `escapeHtml` utilities from shared utils - Remove duplicate `escapeAttribute` function from ModelModal.js - Apply escaping to file path attributes in model modal and trigger words - Escape folder path HTML content to prevent XSS vulnerabilities - Ensure safe handling of user-controlled data in UI components
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
|||||||
} from './ModelMetadata.js';
|
} from './ModelMetadata.js';
|
||||||
import { setupTagEditMode } from './ModelTags.js';
|
import { setupTagEditMode } from './ModelTags.js';
|
||||||
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
||||||
import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
|
import { renderCompactTags, setupTagTooltip, formatFileSize, escapeAttribute, escapeHtml } from './utils.js';
|
||||||
import { renderTriggerWords, setupTriggerWordsEditMode } from './TriggerWords.js';
|
import { renderTriggerWords, setupTriggerWordsEditMode } from './TriggerWords.js';
|
||||||
import { parsePresets, renderPresetTags } from './PresetTags.js';
|
import { parsePresets, renderPresetTags } from './PresetTags.js';
|
||||||
import { initVersionsTab } from './ModelVersionsTab.js';
|
import { initVersionsTab } from './ModelVersionsTab.js';
|
||||||
@@ -65,12 +65,6 @@ function hasLicenseField(license, field) {
|
|||||||
return Object.prototype.hasOwnProperty.call(license || {}, field);
|
return Object.prototype.hasOwnProperty.call(license || {}, field);
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeAttribute(value) {
|
|
||||||
return String(value ?? '')
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
function indentMarkup(markup, spaces) {
|
function indentMarkup(markup, spaces) {
|
||||||
if (!markup) {
|
if (!markup) {
|
||||||
return '';
|
return '';
|
||||||
@@ -266,9 +260,11 @@ export async function showModelModal(model, modelType) {
|
|||||||
...model,
|
...model,
|
||||||
civitai: completeCivitaiData
|
civitai: completeCivitaiData
|
||||||
};
|
};
|
||||||
|
const escapedFilePathAttr = escapeAttribute(modelWithFullData.file_path || '');
|
||||||
|
const escapedFolderPath = escapeHtml((modelWithFullData.file_path || '').replace(/[^/]+$/, '') || 'N/A');
|
||||||
const licenseIcons = renderLicenseIcons(modelWithFullData);
|
const licenseIcons = renderLicenseIcons(modelWithFullData);
|
||||||
const viewOnCivitaiAction = modelWithFullData.from_civitai ? `
|
const viewOnCivitaiAction = modelWithFullData.from_civitai ? `
|
||||||
<div class="civitai-view" title="${translate('modals.model.actions.viewOnCivitai', {}, 'View on Civitai')}" data-action="view-civitai" data-filepath="${modelWithFullData.file_path}">
|
<div class="civitai-view" title="${translate('modals.model.actions.viewOnCivitai', {}, 'View on Civitai')}" data-action="view-civitai" data-filepath="${escapedFilePathAttr}">
|
||||||
<i class="fas fa-globe"></i> ${translate('modals.model.actions.viewOnCivitaiText', {}, 'View on Civitai')}
|
<i class="fas fa-globe"></i> ${translate('modals.model.actions.viewOnCivitaiText', {}, 'View on Civitai')}
|
||||||
</div>`.trim() : '';
|
</div>`.trim() : '';
|
||||||
const creatorInfoAction = modelWithFullData.civitai?.creator ? `
|
const creatorInfoAction = modelWithFullData.civitai?.creator ? `
|
||||||
@@ -472,8 +468,8 @@ export async function showModelModal(model, modelType) {
|
|||||||
<label>${translate('modals.model.metadata.location', {}, 'Location')}</label>
|
<label>${translate('modals.model.metadata.location', {}, 'Location')}</label>
|
||||||
<span class="file-path" title="${translate('modals.model.actions.openFileLocation', {}, 'Open file location')}"
|
<span class="file-path" title="${translate('modals.model.actions.openFileLocation', {}, 'Open file location')}"
|
||||||
data-action="open-file-location"
|
data-action="open-file-location"
|
||||||
data-filepath="${modelWithFullData.file_path}">
|
data-filepath="${escapedFilePathAttr}">
|
||||||
${modelWithFullData.file_path.replace(/[^/]+$/, '') || 'N/A'}
|
${escapedFolderPath}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -506,7 +502,7 @@ export async function showModelModal(model, modelType) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="showcase-section" data-model-hash="${modelWithFullData.sha256 || ''}" data-filepath="${modelWithFullData.file_path}">
|
<div class="showcase-section" data-model-hash="${modelWithFullData.sha256 || ''}" data-filepath="${escapedFilePathAttr}">
|
||||||
<div class="showcase-tabs">
|
<div class="showcase-tabs">
|
||||||
${tabsContent}
|
${tabsContent}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { showToast, copyToClipboard } from '../../utils/uiHelpers.js';
|
import { showToast, copyToClipboard } from '../../utils/uiHelpers.js';
|
||||||
import { translate } from '../../utils/i18nHelpers.js';
|
import { translate } from '../../utils/i18nHelpers.js';
|
||||||
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
||||||
|
import { escapeAttribute } from './utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch trained words for a model
|
* Fetch trained words for a model
|
||||||
@@ -180,11 +181,12 @@ function createSuggestionDropdown(trainedWords, classTokens, existingWords = [])
|
|||||||
* @returns {string} HTML content
|
* @returns {string} HTML content
|
||||||
*/
|
*/
|
||||||
export function renderTriggerWords(words, filePath) {
|
export function renderTriggerWords(words, filePath) {
|
||||||
|
const safeFilePath = escapeAttribute(filePath || '');
|
||||||
if (!words.length) return `
|
if (!words.length) return `
|
||||||
<div class="info-item full-width trigger-words">
|
<div class="info-item full-width trigger-words">
|
||||||
<div class="trigger-words-header">
|
<div class="trigger-words-header">
|
||||||
<label>${translate('modals.model.triggerWords.label')}</label>
|
<label>${translate('modals.model.triggerWords.label')}</label>
|
||||||
<button class="edit-trigger-words-btn metadata-edit-btn" data-file-path="${filePath}" title="${translate('modals.model.triggerWords.edit')}">
|
<button class="edit-trigger-words-btn metadata-edit-btn" data-file-path="${safeFilePath}" title="${translate('modals.model.triggerWords.edit')}">
|
||||||
<i class="fas fa-pencil-alt"></i>
|
<i class="fas fa-pencil-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,7 +209,7 @@ export function renderTriggerWords(words, filePath) {
|
|||||||
<div class="info-item full-width trigger-words">
|
<div class="info-item full-width trigger-words">
|
||||||
<div class="trigger-words-header">
|
<div class="trigger-words-header">
|
||||||
<label>${translate('modals.model.triggerWords.label')}</label>
|
<label>${translate('modals.model.triggerWords.label')}</label>
|
||||||
<button class="edit-trigger-words-btn metadata-edit-btn" data-file-path="${filePath}" title="${translate('modals.model.triggerWords.edit')}">
|
<button class="edit-trigger-words-btn metadata-edit-btn" data-file-path="${safeFilePath}" title="${translate('modals.model.triggerWords.edit')}">
|
||||||
<i class="fas fa-pencil-alt"></i>
|
<i class="fas fa-pencil-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,20 @@
|
|||||||
* Helper functions for the Model Modal component - General version
|
* Helper functions for the Model Modal component - General version
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export function escapeHtml(value = '') {
|
||||||
|
if (value === null || value === undefined) return '';
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function escapeAttribute(value = '') {
|
||||||
|
return escapeHtml(value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format file size
|
* Format file size
|
||||||
* @param {number} bytes - Number of bytes
|
* @param {number} bytes - Number of bytes
|
||||||
@@ -31,6 +45,7 @@ export function formatFileSize(bytes) {
|
|||||||
export function renderCompactTags(tags, filePath = '') {
|
export function renderCompactTags(tags, filePath = '') {
|
||||||
// Remove the early return and always render the container
|
// Remove the early return and always render the container
|
||||||
const tagsList = tags || [];
|
const tagsList = tags || [];
|
||||||
|
const safeFilePath = escapeAttribute(filePath || '');
|
||||||
|
|
||||||
// Display up to 5 tags, with a tooltip indicator if there are more
|
// Display up to 5 tags, with a tooltip indicator if there are more
|
||||||
const visibleTags = tagsList.slice(0, 5);
|
const visibleTags = tagsList.slice(0, 5);
|
||||||
@@ -40,20 +55,20 @@ export function renderCompactTags(tags, filePath = '') {
|
|||||||
<div class="model-tags-container">
|
<div class="model-tags-container">
|
||||||
<div class="model-tags-header">
|
<div class="model-tags-header">
|
||||||
<div class="model-tags-compact">
|
<div class="model-tags-compact">
|
||||||
${visibleTags.map(tag => `<span class="model-tag-compact">${tag}</span>`).join('')}
|
${visibleTags.map(tag => `<span class="model-tag-compact">${escapeHtml(tag)}</span>`).join('')}
|
||||||
${remainingCount > 0 ?
|
${remainingCount > 0 ?
|
||||||
`<span class="model-tag-more" data-count="${remainingCount}">+${remainingCount}</span>` :
|
`<span class="model-tag-more" data-count="${remainingCount}">+${remainingCount}</span>` :
|
||||||
''}
|
''}
|
||||||
${tagsList.length === 0 ? `<span class="model-tag-empty">No tags</span>` : ''}
|
${tagsList.length === 0 ? `<span class="model-tag-empty">No tags</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<button class="edit-tags-btn" data-file-path="${filePath}" title="Edit tags">
|
<button class="edit-tags-btn" data-file-path="${safeFilePath}" title="Edit tags">
|
||||||
<i class="fas fa-pencil-alt"></i>
|
<i class="fas fa-pencil-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
${tagsList.length > 0 ?
|
${tagsList.length > 0 ?
|
||||||
`<div class="model-tags-tooltip">
|
`<div class="model-tags-tooltip">
|
||||||
<div class="tooltip-content">
|
<div class="tooltip-content">
|
||||||
${tagsList.map(tag => `<span class="tooltip-tag">${tag}</span>`).join('')}
|
${tagsList.map(tag => `<span class="tooltip-tag">${escapeHtml(tag)}</span>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
</div>` :
|
</div>` :
|
||||||
''}
|
''}
|
||||||
|
|||||||
Reference in New Issue
Block a user