Validate antiforgery on the admin file manager and fix picture URLs from subfolders - #765
Merged
Merged
Conversation
…oad endpoints
BaseAdminController applies [AutoValidateAntiforgeryToken], but four actions opted
out of it. The worst is ElFinderController.Connector - one action serving the whole
file manager, so upload, rename, delete and paste were all reachable through CSRF
from an authenticated administrator's browser. DownloadController.SaveDownloadUrl
and AsyncUpload were equally open, and LanguageController.Resources opted out for no
reason at all.
The client side needed no new mechanism. elFinder and fineUploader now send
customHeaders: { 'X-CSRF-TOKEN': ... }, the same way Picture.cshtml already did for
an AsyncUpload that never carried the opt-out. Editor.cshtml takes the token from an
injected IAntiforgery rather than from a __RequestVerificationToken input, because
the editor is not always rendered inside a form and GetAndStoreTokens issues the
cookie as well. The language resources grid already called addAntiForgeryToken in
additionalData(), so removing its attribute was enough.
AntiforgeryOptOutTests replaces the hand-kept list: it walks the panel assembly and
fails if any action carries IgnoreAntiforgeryTokenAttribute. Verified that it fails
on the old behaviour, naming exactly those four actions.
The opt-outs in Grand.Module.Api are left alone - TokenController and
TokenWebController are anonymous JSON endpoints issuing JWTs, with no cookie
authentication for CSRF to ride on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Picking a picture out of any subfolder of the media library inserted a URL that 404s: the subdirectory was missing from it. OpenResponse.cwd is declared as BaseInfoResponse while the instance is a DirectoryInfoResponse or RootInfoResponse, which add phash, volumeid and dirs. System.Text.Json serializes the declared type, so those three never reached the browser. elFinder caches cwd in its file map, overwriting the complete entry it already had from files[] with a parentless one; path2array then stops at the current directory instead of walking up to the volume root, and url() builds volume url + file name with every intermediate directory dropped. Files sitting directly in the root were unaffected, which is why this went unnoticed. BaseInfoResponseConverter writes file info by its runtime type and is attached to the connector's JsonResult only, so nothing else in the app changes serialization. It intercepts the declared base type alone, so the nested write resolves through the default converter rather than re-entering. Verified in the browser: cwd now carries phash and volumeid, the path resolves to Volume/test/PHOTO-1.jpg, the URL returns 200 instead of 404, and double-clicking a picture inserts /assets/images/uploaded/test/PHOTO-1.jpg into the editor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type: bugfix
Issue
Two defects in the admin media surface, found while working through the security items in the architecture audit.
1. Four admin actions opted out of antiforgery validation.
BaseAdminControllercarries[AutoValidateAntiforgeryToken], but these actions disabled it:ElFinderController.ConnectorDownloadController.SaveDownloadUrlDownloadController.AsyncUploadLanguageController.ResourcesThe admin panel is enabled in every installation, so exploiting this needed nothing more than luring a signed-in administrator to a foreign page. To reproduce: sign in to
/admin, then from any other origin (or the browser console)POSTto/admin/ElFinder/Connectorwithcmd=mkfile— before this change it succeeds and writes a file.2. Picking a picture from any media subfolder inserted a URL that 404s. The subdirectory was missing from it.
OpenResponse.cwdis declared asBaseInfoResponsewhile the instance is aDirectoryInfoResponseorRootInfoResponse, which addphash,volumeidanddirs. System.Text.Json serializes the declared type, so those three never reached the browser. elFinder cachescwdin its file map, overwriting the complete entry it already had fromfiles[]with a parentless one;path2arraythen stops at the current directory instead of walking up to the volume root, andurl()builds volume url + file name with every intermediate directory dropped. Files sitting directly in the media root were unaffected, which is why this went unnoticed.Solution
Antiforgery. Removed all four
[IgnoreAntiforgeryToken]attributes. The client side needed no new mechanism — the patterns were already in the repository:customHeaders: { 'X-CSRF-TOKEN': ... }, the same wayPicture.cshtmlalready did for anAsyncUploadthat never carried the opt-out. The header name comes fromServiceCollectionExtensions.AddAntiForgery.Editor.cshtmltakes the token from an injectedIAntiforgeryrather than from a__RequestVerificationTokeninput, because the editor is not always rendered inside a form andGetAndStoreTokensissues the cookie as well.addAntiForgeryTokeninadditionalData(), so removing the attribute was enough.AntiforgeryOptOutTestsreplaces the hand-kept list: it walks the panel assembly and fails if any action carriesIgnoreAntiforgeryTokenAttribute. Verified that it fails on the old behaviour, naming exactly those four actions.The opt-outs in
Grand.Module.Apiare left alone —TokenControllerandTokenWebControllerare anonymous JSON endpoints issuing JWTs, with no cookie authentication for CSRF to ride on.Picture URLs.
BaseInfoResponseConverterwrites file info by its runtime type and is attached to the connector'sJsonResultonly, so nothing else in the application changes serialization. It intercepts the declared base type alone, so the nested write resolves through the default converter rather than re-entering.Breaking changes
ElFinderViewModelServicegained a constructor parameter (IOptions<JsonOptions>). Resolved through DI everywhere in the repository, so nothing here changes, but code that constructs or derives from this service directly needs the extra argument.Nothing else: no view model, widget zone, plugin system name, setting, permission or localization resource changed.
Testing
dotnet build ./GrandNode.slndotnet test src/Tests/Grand.Web.Admin.Tests— 55 pass, including the three new converter tests and the antiforgery guard./admin. In the browser console run:/admin/Download/SaveDownloadUrland/admin/Language/Resources— both 400.wwwroot/assets/images/uploaded(e.g.uploaded/test/). In the file manager open that subfolder and double-click the image: the editor receives/assets/images/uploaded/test/<name>and the image renders. Before this change the inserted URL was/assets/images/uploaded/<name>and returned 404.🤖 Generated with Claude Code