Repository navigation
fix(security): authenticate the OnlyOffice save callback - #2016
Merged
Merged
Conversation
POST /api/translation-downloads/pptx-callback had no authentication and took the slide from its query string: anyone could overwrite the proofread presentation of any slide, and the unchecked partId steered the S3 key. The editor now asks the API for a per-slide token (POST /api/translation-downloads/pptx-callback-token, ids restricted to single path segments, slide checked in the database) and passes it in the callback URL. The callback refuses requests without a known, unexpired token and takes the slide from the token only. The Document Server in docker/compose.yml no longer fetches private or cloud metadata addresses.
The REST translation routes (uploads, start/retry, jobs, text-to-speech, proofread copies, PNG generation, editor downloads, PNG viewer) only required a session, so any student could delete and replace a course's source files, start paid translations or generate audio with a professor's voice. Upload and job routes now require the admin role, the slide editing routes the contributor role (createExpressRoleMiddleware: 401 without session, 403 when the role is not enough). The translated PNG viewer turned the client-supplied ppt_resource_path into an S3 key, so any object of the bucket could be read. The key builders now refuse anything but contribute/.../pptx/<name>.pptx.
fix(security): restrict translation tooling to contributors and admins
| // the builders replace the first occurrence of each | ||
| pptResourcePath.indexOf('/pptx/') === | ||
| pptResourcePath.lastIndexOf('/pptx/') && | ||
| pptResourcePath.indexOf('.pptx') === pptResourcePath.length - 5; |
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.
Vulnerability
POST /api/translation-downloads/pptx-callbackis the save callback of the OnlyOffice editor used by the translation tool. It had no authentication: the Document Server calls it without a session. It took the slide (courseId,language,partId,chapterId,slideId) from the query string and the documenturlfrom the body, then downloaded that document from the OnlyOffice host and wrote it to S3 undercontribute/<courseId>/<language>/<partId>/<chapterId>/<slideId>/pptx/<name>-proofread.pptx.partIdwas never checked, so the S3 key could be steered to another location undercontribute/.In
docker/compose.yml, the Document Server also runs withALLOW_PRIVATE_IP_ADDRESS=trueandALLOW_META_IP_ADDRESS=true, which lets anyone who can reach it make it fetch internal addresses of the Docker network and the cloud metadata address.Fix
The callback is authenticated by a per-slide token issued by the API.
POST /api/translation-downloads/pptx-callback-token(logged-in users, same guard as the other editor routes). It checks that each id is a single path segment ([A-Za-z0-9_-]+), that the slide exists incontent.course_translation_slides, and returns a random 256-bit token. The token is kept in memory for 24 hours with the slide it was issued for, like the existingpptx-tokendownload tokens.apps/contribute/.../onlyoffice-slide-editor.tsx) asks for a token before opening and gives OnlyOffice…/pptx-callback?token=<token>ascallbackUrl.pptx-callbackanswers403 {"error":1}without a known, unexpired token. The slide and the S3 key come from the token only; query parameters are ignored. The callback still writes only the-proofread.pptxcopy of that slide, never the original.unref()'d so it does not keep a process alive by itself.OnlyOffice no longer fetches private or metadata addresses:
ALLOW_PRIVATE_IP_ADDRESS=falseandALLOW_META_IP_ADDRESS=falseindocker/compose.yml. The local developmentcompose.ymlkeeps its settings, because it reaches the API throughhost.docker.internal.OnlyOffice's own JWT (
JWT_ENABLED) would be the stronger option, but it requires the API to sign the whole editor configuration server-side, which is a larger change of the editor flow. The token gives the callback a credential without that.Tests
apps/apigets a mocha setup likesharedandservice-user(.mocharc.json+testscript,pnpm --filter @blms/api test).apps/api/src/routers/rest/translation-callback.test.ts(express on127.0.0.1:0, fake database, fake S3, outboundfetchstubbed):refuses a callback without a token, whatever its query says: ondev,200 == 403and the forged document is written to S3. Passes here (403, no S3 write, no fetch to OnlyOffice).refuses a callback with an unknown token: ondev,200 == 403. Passes here.saves only the proofread copy of the slide the token was issued for(extrapartId/courseIdin the query are ignored): fails ondev(the token route does not exist), passes here.refuses to issue a token for ids that are not single path segments: fails ondev(404 == 400), passes here.turbo run type-check --filter=@blms/api --filter=@blms/contributepasses andbiome checkis clean on the changed files (one pre-existinguseExhaustiveDependenciesinfo in the editor).Behaviour changes and deploy notes
docker/compose.yml, the Document Server must reach the document and callback URLs through public addresses (they are built from the browser's host, which is public in production).