-
-
Notifications
You must be signed in to change notification settings - Fork 3k
security(export): strip remote images on the soffice path to match the native path #8071
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
JohnMcLear
wants to merge
2
commits into
develop
Choose a base branch
from
security/soffice-export-remote-image-ssrf
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+146
−1
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| 'use strict'; | ||
|
|
||
| /** | ||
| * SSRF regression for the LibreOffice (`soffice`) export path. | ||
| * | ||
| * The native (in-process) export path strips remote `<img>` tags before | ||
| * rendering, but the `soffice` path historically wrote the full export HTML to | ||
| * disk verbatim and handed it to LibreOffice, which fetches remote image URLs | ||
| * during conversion — a blind SSRF sink reachable whenever a plugin/hook injects | ||
| * image tags into export HTML. Reported by `meifukun`. | ||
| * | ||
| * This test drives the real export handler with: | ||
| * - an `exportHTMLAdditionalContent` hook that injects a remote image (the | ||
| * documented plugin extension point from the PoC), and | ||
| * - an `exportConvert` hook that stands in for LibreOffice: it captures the | ||
| * exact HTML written to the temp file and writes a dummy output so the | ||
| * request completes. | ||
| * It then asserts the captured HTML contains no remote image URL. | ||
| */ | ||
|
|
||
| import {MapArrayType} from "../../../node/types/MapType"; | ||
| import fs from 'node:fs'; | ||
| const fsp = fs.promises; | ||
|
|
||
| const assert = require('assert').strict; | ||
| const common = require('../common'); | ||
| const padManager = require('../../../node/db/PadManager'); | ||
| const plugins = require('../../../static/js/pluginfw/plugin_defs'); | ||
| import settings from '../../../node/utils/Settings'; | ||
|
|
||
| const REMOTE_IMG = '<img src="http://127.0.0.1:39111/ssrf-marker-soffice" alt="probe">'; | ||
|
|
||
| describe(__filename, function () { | ||
| this.timeout(30000); | ||
| let agent: any; | ||
| const settingsBackup: MapArrayType<any> = {}; | ||
| const hookBackup: MapArrayType<any> = {}; | ||
| const hookNames = ['exportHTMLAdditionalContent', 'exportConvert']; | ||
| let capturedHtml = ''; | ||
|
|
||
| const makeHook = (hookName: string, hookFn: Function) => ({ | ||
| hook_fn: hookFn, | ||
| hook_fn_name: `ssrf_test/${hookName}`, | ||
| hook_name: hookName, | ||
| part: {plugin: 'ssrf_test'}, | ||
| }); | ||
|
|
||
| before(async function () { | ||
| agent = await common.init(); | ||
| settingsBackup.soffice = settings.soffice; | ||
| await padManager.getPad('sofficeSsrfPad', 'test content'); | ||
| }); | ||
|
|
||
| beforeEach(function () { | ||
| for (const h of hookNames) { | ||
| hookBackup[h] = plugins.hooks[h]; | ||
| } | ||
| capturedHtml = ''; | ||
| // Inject a remote image the way a plugin extension point would. | ||
| plugins.hooks.exportHTMLAdditionalContent = [ | ||
| makeHook('exportHTMLAdditionalContent', () => REMOTE_IMG), | ||
| ]; | ||
| // Stand in for LibreOffice: capture what was written, produce a dummy file. | ||
| plugins.hooks.exportConvert = [ | ||
| makeHook('exportConvert', async (hookName: string, {srcFile, destFile}: any) => { | ||
| capturedHtml = await fsp.readFile(srcFile, 'utf8'); | ||
| await fsp.writeFile(destFile, 'dummy-converted-output'); | ||
| return 'handled'; | ||
| }), | ||
| ]; | ||
| // Force the soffice conversion path (non-null => sofficeAvailable() === 'yes' | ||
| // on non-Windows). The binary is never actually spawned because the | ||
| // exportConvert hook short-circuits the converter. | ||
| settings.soffice = '/usr/bin/soffice'; | ||
| }); | ||
|
|
||
| afterEach(function () { | ||
| for (const h of hookNames) { | ||
| plugins.hooks[h] = hookBackup[h]; | ||
| } | ||
| }); | ||
|
|
||
| after(function () { | ||
| Object.assign(settings, settingsBackup); | ||
| }); | ||
|
|
||
| it('confirms the injected remote image reaches the export HTML', async function () { | ||
| // Sanity check on the harness: the raw HTML export (which is NOT run through | ||
| // the soffice temp-file path) must contain the injected image, proving the | ||
| // hook fires and the marker would otherwise be present. | ||
| const res = await agent.get('/p/sofficeSsrfPad/export/html').expect(200); | ||
| assert.ok( | ||
| res.text.includes('ssrf-marker-soffice'), | ||
| 'expected the injected remote image to appear in the html export'); | ||
| }); | ||
|
|
||
| it('strips remote images from the HTML handed to the soffice converter', | ||
| async function () { | ||
| await agent.get('/p/sofficeSsrfPad/export/odt').expect(200); | ||
| assert.ok(capturedHtml.length > 0, 'exportConvert hook did not capture any HTML'); | ||
| assert.ok( | ||
| !capturedHtml.includes('ssrf-marker-soffice'), | ||
| `soffice temp file still contains the remote image URL:\n${capturedHtml}`); | ||
| assert.ok( | ||
| !/<img[^>]+src\s*=\s*["']?https?:/i.test(capturedHtml), | ||
| `soffice temp file still contains a remote <img src="http...">:\n${capturedHtml}`); | ||
| }); | ||
| }); |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.