CVE-2022-46165 (HTML, published 2023-06-06T18:15Z)
Description: value: Syncthing is an open source, continuous file synchronization program. In versions prior to 1.23.5 a compromised instance with shared folders could sync malicious files which contain arbitrary HTML and JavaScript in the name. If the owner of another device looks over the shared folder settings and moves the mouse over the latest sync, a script could be executed to change settings for shared folders or add devices automatically. Additionally adding a new device with a malicious name could embed HTML or JavaScript inside parts of the page. As a result the webUI may be subject to a stored cross site scripting attack. This issue has been addressed in version 1.23.5. Users are advised to upgrade. Users unable to upgrade should avoid sharing folders with untrusted users.
Severity: nan (CVSS v3 base score: 5.4)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/syncthing/syncthing
Files changed:
gui/default/assets/css/overrides.css
gui/default/index.html
gui/default/syncthing/core/aboutModalView.html
gui/default/syncthing/core/editShareTemplate.html
gui/default/syncthing/core/syncthingController.js
gui/default/syncthing/device/editDeviceModalView.html
gui/default/syncthing/device/globalChangesModalView.html
gui/default/syncthing/folder/editFolderModalView.html
gui/default/syncthing/transfer/localChangedFilesModalView.html
gui/default/syncthing/transfer/remoteNeededFilesModalView.html
Commit message: gui: Avoid code generating HTML (#8923)



Vulnerable code:
.file-path {
                <span ng-bind-html="friendlyDevices(err.message) | linky: '_blank'"></span>
                        <td class="text-right">
                          <span tooltip data-original-title="{{sharesFolder(folder)}} {{folderHasUnacceptedDevices(folder) ? '<br/>(<sup>1</sup>' + ('The remote device has not accepted sharing this folder.' | translate) + ')' : ''}} {{folderHasPausedDevices(folder) ? '<br/>(<sup>2</sup>' + ('The remote device has paused this folder.' | translate) + ')' : ''}}" ng-bind-html="sharesFolder(folder)"></span>
                        <td class="text-right">
                          <span tooltip data-original-title="{{sharedFolders(deviceCfg)}} {{deviceHasUnacceptedFolders(deviceCfg) ? '<br/>(<sup>1</sup>' + ('The remote device has not accepted sharing this folder.' | translate) + ')' : '' }} {{deviceHasPausedFolders(deviceCfg) ? '<br/>(<sup>2</sup>' + ('The remote device has paused this folder.' | translate) + ')' : '' }}" ng-bind-html="sharedFolders(deviceCfg)"></span>
              <td><code class="file-path">{{ about.paths['baseDir-userHome'] }}</code></td>
              <td><code class="file-path"><strong>{{ about.paths['baseDir-config'] }}</strong></code></td>
              <td><code class="file-path">{{ about.paths['config'] }}</code></td>
              <td><code class="file-path">{{ about.paths['certFile'] }}</code>
                <br /><code class="file-path">{{ about.paths['keyFile'] }}</code></td>
              <td><code class="file-path">{{ about.paths['httpsCertFile'] }}</code>
                <br /><code class="file-path">{{ about.paths['httpsKeyFile'] }}</code></td>
              <td><code class="file-path">{{ about.paths['database'] }}</code></td>
              <td><code class="file-path">{{ about.paths['logFile'] }}</code></td>
              <td><code class="file-path">{{ about.paths['guiAssets'] }}</code></td>
    <span tooltip data-original-title="{{id}}" ng-bind-html="label"></span>
        $scope.deviceNameMarkRemoteState = function (deviceID, folderID) {
            var name = $scope.deviceName($scope.devices[deviceID]);
            // Add footnote if sharing was not accepted on the remote device
            if (deviceID in $scope.completion && folderID in $scope.completion[deviceID]) {
                if ($scope.completion[deviceID][folderID].remoteState == 'notSharing') {
                    name += '<sup>1</sup>';
                } else if ($scope.completion[deviceID][folderID].remoteState == 'paused') {
                    name += '<sup>2</sup>';
                }
            }
            return name;
        };

        $scope.sharesFolder = function (folderCfg) {
            var names = [];
            folderCfg.devices.forEach(function (device) {
                if (device.deviceID !== $scope.myID) {
                    names.push($scope.deviceNameMarkRemoteState(device.deviceID, folderCfg.id));
                }
            });
            names.sort();
            return names.join(", ");
        };

        $scope.folderLabelMarkRemoteState = function (folderID, deviceID) {
            var label = $scope.folderLabel(folderID);
            // Add footnote if sharing was not accepted on the remote device
            if (deviceID in $scope.completion && folderID in $scope.completion[deviceID]) {
                if ($scope.completion[deviceID][folderID].remoteState == 'notSharing') {
                    label += '<sup>1</sup>';
                } else if ($scope.completion[deviceID][folderID].remoteState == 'paused') {
                    label += '<sup>2</sup>';
                }
            }
            return label;
        };

        $scope.sharedFolders = function (deviceCfg) {
            var labels = [];
            $scope.deviceFolders(deviceCfg).forEach(function (folderID) {
                labels.push($scope.folderLabelMarkRemoteState(folderID, deviceCfg.deviceID));
            });
            return labels.join(', ');
        };

                <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{folder.id}}" label="{{folderLabelMarkRemoteState(folder.id, currentDevice.deviceID)}}" folder-type="{{folder.type}}" untrusted="currentDevice.untrusted" />
            <td class="file-path no-overflow-ellipse">{{changeEvent.data.path}}</td>
              <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{device.deviceID}}" label="{{deviceNameMarkRemoteState(device.deviceID, currentFolder.id)}}" folder-type="{{currentFolder.type}}" untrusted="device.untrusted || pendingIsRemoteEncrypted(currentFolder.id, device.deviceID)" />
        <td class="file-path">{{file.name}}</td>
                <td class="file-path">{{file.name}}</td>

Fixed code:
.word-break-all {
                <span ng-bind="friendlyDevices(err.message)"></span>
                        <td class="text-right no-overflow-ellipse word-break-all">
                          <span ng-repeat="device in folder.devices">
                            <span ng-if="device.deviceID != myID" ng-switch="completion[device.deviceID][folder.id].remoteState">
                              <span ng-switch-when="notSharing" data-original-title="{{'The remote device has not accepted sharing this folder.' | translate}}" tooltip>{{deviceName(devices[device.deviceID])}}<sup>1</sup><span ng-if="!$last">,</span></span>
                              <span ng-switch-when="paused" data-original-title="{{'The remote device has paused this folder.' | translate}}" tooltip>{{deviceName(devices[device.deviceID])}}<sup>2</sup><span ng-if="!$last">,</span></span>
                              <span ng-switch-default>{{deviceName(devices[device.deviceID])}}<span ng-if="!$last">,</span></span>
                            </span>
                          </span>
                        <td class="text-right no-overflow-ellipse word-break-all">
                          <span ng-repeat="folderID in deviceFolders(deviceCfg)">
                            <span ng-switch="completion[deviceCfg.deviceID][folderID].remoteState">
                              <span ng-switch-when="notSharing" data-original-title="{{'The remote device has not accepted sharing this folder.' | translate}}" tooltip>{{folderLabel(folderID)}}<sup>1</sup><span ng-if="!$last">,</span></span>
                              <span ng-switch-when="paused" data-original-title="{{'The remote device has paused this folder.' | translate}}" tooltip>{{folderLabel(folderID)}}<sup>2</sup><span ng-if="!$last">,</span></span>
                              <span ng-switch-default>{{folderLabel(folderID)}}<span ng-if="!$last">,</span></span>
                            </span>
                          </span>
              <td><code class="word-break-all">{{ about.paths['baseDir-userHome'] }}</code></td>
              <td><code class="word-break-all"><strong>{{ about.paths['baseDir-config'] }}</strong></code></td>
              <td><code class="word-break-all">{{ about.paths['config'] }}</code></td>
              <td><code class="word-break-all">{{ about.paths['certFile'] }}</code>
                <br /><code class="word-break-all">{{ about.paths['keyFile'] }}</code></td>
              <td><code class="word-break-all">{{ about.paths['httpsCertFile'] }}</code>
                <br /><code class="word-break-all">{{ about.paths['httpsKeyFile'] }}</code></td>
              <td><code class="word-break-all">{{ about.paths['database'] }}</code></td>
              <td><code class="word-break-all">{{ about.paths['logFile'] }}</code></td>
              <td><code class="word-break-all">{{ about.paths['guiAssets'] }}</code></td>
    <span tooltip data-original-title="{{id}}">
      <span ng-switch="remoteState">
        <span ng-switch-when="notSharing">{{label}}<sup>1</sup></span>
        <span ng-switch-when="paused">{{label}}<sup>2</sup></span>
        <span ng-switch-default>{{label}}</span>
      </span>
    </span>
                remoteState: '@',
                <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{folder.id}}" label="{{folderLabel(folder.id)}}" folder-type="{{folder.type}}" untrusted="currentDevice.untrusted" remote-state="{{completion[currentDevice.deviceID][folder.id].remoteState}}" />
            <td class="word-break-all no-overflow-ellipse">{{changeEvent.data.path}}</td>
              <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{device.deviceID}}" label="{{deviceName(device)}}" folder-type="{{currentFolder.type}}" untrusted="device.untrusted || pendingIsRemoteEncrypted(currentFolder.id, device.deviceID)" remote-state="{{completion[device.deviceID][currentFolder.id].remoteState}}" />
        <td class="word-break-all">{{file.name}}</td>
                <td class="word-break-all">{{file.name}}</td>