Skip to content
Merged
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
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.028"
VERSION = "0.261.029"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@
}
}

window.bindGroupDocumentShareButton = function(container, doc) {

Check warning on line 79 in application/single_app/static/js/workspace/group-documents-sharing.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
const shareButton = container.querySelector('.group-document-share-btn');
if (!shareButton) {
return;
}

const documentId = doc.id;
const fileName = doc.file_name || '';
shareButton.addEventListener('click', function(event) {

Check warning on line 87 in application/single_app/static/js/workspace/group-documents-sharing.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
event.preventDefault();
window.shareGroupDocument(documentId, fileName);
});
};

// Main function to open share modal
window.shareGroupDocument = function(documentId, fileName) {
currentGroupDocumentId = documentId;
Expand Down
25 changes: 20 additions & 5 deletions application/single_app/templates/group_workspaces.html
Original file line number Diff line number Diff line change
Expand Up @@ -4140,7 +4140,7 @@ <h5 class="modal-title" id="workflowDeleteModalLabel">Delete Group Workflow</h5>
if (canShare) {
const shareCount = Array.isArray(doc.shared_group_ids) ? doc.shared_group_ids.length : 0;
dropdownItems += `
<li><a class="dropdown-item" href="#" onclick="shareGroupDocument('${docId}', '${escapeGroupHtml(doc.file_name || '')}'); return false;">
<li><a class="dropdown-item group-document-share-btn" href="#">
<i class="bi bi-share-fill me-2"></i>Share
<span class="badge bg-secondary ms-1">${shareCount}</span>
</a></li>`;
Expand Down Expand Up @@ -4199,8 +4199,7 @@ <h5 class="modal-title" id="workflowDeleteModalLabel">Delete Group Workflow</h5>
<div class="item-card-icon"><i class="bi ${getGroupDocumentIcon(doc.file_name || '')}" style="font-size: 1.75rem;"></i></div>
<div class="document-item-card__title-wrap">
<div class="document-item-card__eyebrow">Group document</div>
<h6 class="card-title mb-1" title="${escapeGroupHtml(displayTitle)}">${escapeGroupHtml(truncateGroupDocumentText(displayTitle, 60))}</h6>
${subtitle ? `<div class="document-item-card__subtitle" title="${escapeGroupHtml(subtitle)}">${escapeGroupHtml(subtitle)}</div>` : ''}
<h6 class="card-title mb-1"></h6>
</div>
<div class="document-item-card__status">${statusBadge}</div>
</div>
Expand All @@ -4220,6 +4219,18 @@ <h6 class="card-title mb-1" title="${escapeGroupHtml(displayTitle)}">${escapeGro
</div>
</div>`;

const titleElement = column.querySelector('.card-title');
titleElement.title = displayTitle;
titleElement.textContent = truncateGroupDocumentText(displayTitle, 60);
if (subtitle) {
const subtitleElement = document.createElement('div');
subtitleElement.className = 'document-item-card__subtitle';
subtitleElement.title = subtitle;
subtitleElement.textContent = subtitle;
titleElement.after(subtitleElement);
}
window.bindGroupDocumentShareButton(column, doc);

return column;
}

Expand Down Expand Up @@ -6204,7 +6215,7 @@ <h5 class="modal-title" id="groupDocumentDeleteModalLabel">Delete Group Document
if (canShare) {
actionsDropdown += `
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="shareGroupDocument('${docId}', '${escapeHtml(doc.file_name || '')}'); return false;">
<li><a class="dropdown-item group-document-share-btn" href="#">
<i class="bi bi-share-fill me-2"></i>Share
<span class="badge bg-secondary ms-1">${doc.shared_group_ids ? doc.shared_group_ids.length : 0}</span>
</a></li>
Expand Down Expand Up @@ -6284,6 +6295,7 @@ <h5 class="modal-title" id="groupDocumentDeleteModalLabel">Delete Group Document
</td>
`;

window.bindGroupDocumentShareButton(docRow, doc);
const actionsCell = docRow.querySelector("td:last-child");
prependGroupGeneratedArtifactActionButtons(actionsCell, doc, userRole);

Expand Down Expand Up @@ -8151,7 +8163,7 @@ <h5 class="alert-heading">
if (canManage && access.isOwnerGroup) {
const canShare = (groupStatus === 'active' || groupStatus === 'upload_disabled');
if (canShare) {
actionsDropdown += `<li><hr class="dropdown-divider"></li><li><a class="dropdown-item" href="#" onclick="shareGroupDocument('${docId}', '${escapeHtml(doc.file_name || '')}'); return false;"><i class="bi bi-share-fill me-2"></i>Share<span class="badge bg-secondary ms-1">${doc.shared_group_ids ? doc.shared_group_ids.length : 0}</span></a></li>`;
actionsDropdown += `<li><hr class="dropdown-divider"></li><li><a class="dropdown-item group-document-share-btn" href="#"><i class="bi bi-share-fill me-2"></i>Share<span class="badge bg-secondary ms-1">${doc.shared_group_ids ? doc.shared_group_ids.length : 0}</span></a></li>`;
}
const canDelete = (groupStatus === 'active' || groupStatus === 'upload_disabled');
if (canDelete) {
Expand Down Expand Up @@ -8293,6 +8305,9 @@ <h5 class="alert-heading">

// xss-check: ignore reviewed folder view shell assembled from local escaped table/card builders.
container.innerHTML = html;
container.querySelectorAll('#group-folder-docs-table tbody tr').forEach((row, index) => {
window.bindGroupDocumentShareButton(row, docs[index]);
});
wireGroupBackButton(container);
if (groupCurrentView === 'folders-cards' && docs.length > 0) {
renderGroupFolderDocumentCards(docs);
Expand Down
65 changes: 65 additions & 0 deletions docs/explanation/fixes/GROUP_DOCUMENT_FILENAME_XSS_FIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Group Document Filename XSS Fix (v0.261.029)

Fixed in version: **0.261.029**

Related advisory: [GHSA-qwcw-r653-j8c6](https://github.com/microsoft/simplechat/security/advisories/GHSA-qwcw-r653-j8c6).

Check warning on line 5 in docs/explanation/fixes/GROUP_DOCUMENT_FILENAME_XSS_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains external connection or remote asset marker. Recommendation%3A Review whether changed code can send prompts, files, credentials, cookies, settings, logs, or user data to a new sink.
The application patch version is recorded in `application/single_app/config.py`.

## Issue

A stored group document filename could become executable JavaScript when an eligible group member clicked Share. The vulnerable controls appeared in ordinary list rows, document cards, and folder list rows. Folder cards reused the same affected card renderer.

The card renderer also inserted filenames and document titles into tooltip attributes without a safe attribute boundary. This allowed a separate event-handler injection path on hover, including for ordinary group Users who could not share documents.

Check warning on line 12 in docs/explanation/fixes/GROUP_DOCUMENT_FILENAME_XSS_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

The paths remained present in the inspected `v0.261.001`, `v0.261.027`, and `0.261.028` revisions. Earlier [sharing-modal hardening](v0.241.022/STORED_XSS_SHARE_ACTIVITY_AND_MASKING_FIX.md) protected content inside the modal but did not repair the controls that opened it.

Check warning on line 14 in docs/explanation/fixes/GROUP_DOCUMENT_FILENAME_XSS_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

## Root Cause

Share controls interpolated the stored filename into an inline `onclick` handler. HTML entity escaping was insufficient because the browser decoded the entities before compiling the JavaScript handler.

Check warning on line 18 in docs/explanation/fixes/GROUP_DOCUMENT_FILENAME_XSS_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains dynamic execution, persistence, or system access marker. Recommendation%3A Do not execute changed lifecycle scripts or installers while this finding is unresolved.

Card headings and filename subtitles used an HTML text escaper inside double-quoted `title` attributes. That escaper did not encode quotation marks, allowing data to escape the intended attribute.

## Changes

| File | Change |
|---|---|
| `application/single_app/templates/group_workspaces.html` | Removes all three filename-bearing inline Share handlers, binds each newly rendered control, and populates card titles/subtitles through DOM properties. |

Check warning on line 26 in docs/explanation/fixes/GROUP_DOCUMENT_FILENAME_XSS_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
| `application/single_app/static/js/workspace/group-documents-sharing.js` | Adds a shared event-binding helper that captures the original document ID and filename as data and passes them directly to the existing sharing modal. |

Check warning on line 27 in docs/explanation/fixes/GROUP_DOCUMENT_FILENAME_XSS_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
| `functional_tests/test_group_document_filename_xss.py` | Guards the rendering boundaries, all three binding locations, and the minimum implementation version. |
| `ui_tests/test_group_document_filename_xss_rendering.py` | Exercises actual rendered controls, tooltips, permissions, refreshes, and polling in an isolated browser. |

Check warning on line 29 in docs/explanation/fixes/GROUP_DOCUMENT_FILENAME_XSS_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

Check warning on line 29 in docs/explanation/fixes/GROUP_DOCUMENT_FILENAME_XSS_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
| `application/single_app/config.py` | Increments the application patch version from `0.261.028` to `0.261.029`. |

The Share links now contain static action markup rather than executable document data. Their event listeners prevent link navigation and invoke the existing modal with the unchanged filename. Card text uses `textContent`; tooltips use the DOM `title` property.

Bindings are installed when cards and ordinary rows are created and after folder table markup is inserted. Polling completion uses the same ordinary row renderer, so replacement rows receive the same safe behavior.

## Behavior and Impact

Original filenames remain unchanged in storage, API responses, visible text, tooltips, and sharing dialogs. No document migration or filename rewriting is required, and previously stored filenames receive the same protection as new uploads.

Existing Share permissions, owning-group restrictions, processing/error checks, group status restrictions, and share-count badges are preserved. The fix does not change CSP, introduce browser dependencies, or add settings or routes. The event-binding helper is served from the existing local sharing asset.

## Validation

Before the fix, four focused browser cases executed the injected marker: ordinary-list and folder-list Share clicks, and ordinary-card and folder-card hover. After the fix, all 74 browser scenarios pass:

- Share clicks in list, card, folder-list, and folder-card views preserve literal filenames containing quotes, HTML-like text, entities, backslashes, and Unicode.
- Card heading, subtitle, and document-title tooltips remain inert for ordinary Users at desktop and mobile widths.
- Owner, Admin, DocumentManager, and User roles retain their existing behavior, including receiving-group restrictions and active, upload-disabled, locked, inactive, processing, and error states.
- Refreshes and view changes select the correct document without duplicate handlers; polling completion binds the replacement row.
- Empty filenames retain their previous fallback behavior.

The existing sharing-modal functional regression also passes. That regression alone does not cover the launcher vulnerability because it checks the modal's rendering rather than clicking a filename-bearing launcher.

Run the focused coverage with:

```powershell
python -B -m pytest -q .\functional_tests\test_group_document_filename_xss.py .\ui_tests\test_group_document_filename_xss_rendering.py ".\functional_tests\test_stored_xss_share_activity_and_masking_fix.py::test_document_share_modals_use_safe_rendering_and_delegated_clicks"
node --check .\application\single_app\static\js\workspace\group-documents-sharing.js
```

### Scope and limitations

Browser coverage uses the real template rendering functions, access/processing checks, fetch/refresh/polling paths, local sharing asset, Bootstrap, and modal markup. Unrelated metadata, selection, and generated-artifact helpers are stubbed. Every request is intercepted; the suite does not upload live documents, require authentication, or provision Azure resources. This is isolated Chromium coverage, not an authenticated Azure deployment test.

The complete historical sharing/activity/masking script has separate pre-existing failures involving chat source expectations and an exact historical version assertion. Those unrelated checks were not changed by this fix.
58 changes: 58 additions & 0 deletions functional_tests/test_group_document_filename_xss.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# test_group_document_filename_xss.py
"""
Regression guards for group document filename rendering.
Version: 0.261.029
Implemented in: 0.261.029

These guards cover Share launcher wiring and card tooltip boundaries.
Behavioral coverage lives in ui_tests/test_group_document_filename_xss_rendering.py.
"""

from pathlib import Path
import re
import unittest

from test_support.versioning import assert_app_version_at_least


ROOT = Path(__file__).resolve().parents[1]
TEMPLATE = ROOT / "application" / "single_app" / "templates" / "group_workspaces.html"


class GroupDocumentFilenameRenderingTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.source = TEMPLATE.read_text(encoding="utf-8")

def test_share_launchers_do_not_compile_document_data_as_javascript(self):
handlers = re.findall(r'\bonclick="([^"]*)"', self.source)
share_handlers = [handler for handler in handlers if "shareGroupDocument(" in handler]
self.assertEqual(share_handlers, [], "Share must use a DOM event listener.")
self.assertEqual(
self.source.count('class="dropdown-item group-document-share-btn"'),
3,
"Cards, ordinary rows, and folder rows must use inert Share controls.",
)

def test_share_binding_covers_cards_rows_and_folder_insertion(self):
for binding in (
"window.bindGroupDocumentShareButton(column, doc)",
"window.bindGroupDocumentShareButton(docRow, doc)",
"window.bindGroupDocumentShareButton(row, docs[index])",
):
with self.subTest(binding=binding):
self.assertIn(binding, self.source)

def test_card_titles_are_not_interpolated_into_html_attributes(self):
unsafe_attributes = re.findall(
r'title="\$\{[^}\n]*\b(?:displayTitle|subtitle)\b[^}\n]*\}"',
self.source,
)
self.assertEqual(unsafe_attributes, [], "Card tooltips must use DOM properties.")

def test_implementation_version_is_present(self):
assert_app_version_at_least("0.261.029")


if __name__ == "__main__":
unittest.main()
Loading
Loading