mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-07-02 00:17:40 +08:00
feat(libreoffice): block linked content from untrusted locations
This commit is contained in:
@@ -70,11 +70,13 @@ func (p *libreOfficeProcess) Start(logger *slog.Logger) error {
|
||||
|
||||
// LibreOffice fetches external content (OOXML images via
|
||||
// TargetMode=External, RTF INCLUDEPICTURE, ODT linked images) inside
|
||||
// its own libcurl. Route those fetches through the in-process proxy
|
||||
// so the chromium/webhook SSRF filters apply.
|
||||
if err := writeSofficeProxyConfig(userProfileDirPath, proxy.Addr()); err != nil {
|
||||
// its own libcurl. The profile config routes those fetches through the
|
||||
// in-process proxy so the chromium/webhook SSRF filters apply, and
|
||||
// blocks content linked from untrusted locations so absolute-path
|
||||
// (file://) and direct fetches are dropped at the source.
|
||||
if err := writeSofficeProfileConfig(userProfileDirPath, proxy.Addr()); err != nil {
|
||||
_ = proxy.Stop(context.Background())
|
||||
return fmt.Errorf("write soffice proxy config: %w", err)
|
||||
return fmt.Errorf("write soffice profile config: %w", err)
|
||||
}
|
||||
sofficeEnv := sofficeProxyEnv(os.Environ(), proxy.Addr())
|
||||
|
||||
|
||||
@@ -245,13 +245,26 @@ var hopByHopHeaders = []string{
|
||||
"Upgrade",
|
||||
}
|
||||
|
||||
// sofficeProxyConfigTmpl is the registrymodifications.xcu fragment that
|
||||
// tells soffice's UCB layer to route every HTTP and HTTPS fetch through
|
||||
// proxyHost:proxyPort. The %s placeholders accept the proxy host and
|
||||
// port respectively (host first, port second, repeated for HTTP and
|
||||
// HTTPS).
|
||||
const sofficeProxyConfigTmpl = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
// sofficeProfileConfigTmpl is the registrymodifications.xcu the soffice
|
||||
// daemon loads at startup. It does two things:
|
||||
//
|
||||
// 1. Routes every HTTP and HTTPS fetch through proxyHost:proxyPort so
|
||||
// soffice's own libcurl fetches hit the in-process SSRF proxy.
|
||||
// 2. Sets BlockUntrustedRefererLinks so soffice refuses to load content
|
||||
// linked from a document that sits in an untrusted location.
|
||||
//
|
||||
// The second setting closes the local-read and direct-fetch vectors the
|
||||
// proxy cannot see. A document that links an absolute path
|
||||
// (file:///etc/...) or any URL is loaded from the per-request temp dir,
|
||||
// which is never a trusted location, so soffice drops the linked content
|
||||
// instead of resolving it. Embedded content (stored inside the document)
|
||||
// is unaffected.
|
||||
//
|
||||
// The %s placeholders accept the proxy host and port respectively (host
|
||||
// first, port second, repeated for HTTP and HTTPS).
|
||||
const sofficeProfileConfigTmpl = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<oor:items xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<item oor:path="/org.openoffice.Office.Common/Security/Scripting"><prop oor:name="BlockUntrustedRefererLinks" oor:op="fuse"><value>true</value></prop></item>
|
||||
<item oor:path="/org.openoffice.Inet/Settings"><prop oor:name="ooInetProxyType" oor:op="fuse"><value>1</value></prop></item>
|
||||
<item oor:path="/org.openoffice.Inet/Settings"><prop oor:name="ooInetHTTPProxyName" oor:op="fuse"><value>%s</value></prop></item>
|
||||
<item oor:path="/org.openoffice.Inet/Settings"><prop oor:name="ooInetHTTPProxyPort" oor:op="fuse"><value>%s</value></prop></item>
|
||||
@@ -261,10 +274,11 @@ const sofficeProxyConfigTmpl = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
</oor:items>
|
||||
`
|
||||
|
||||
// writeSofficeProxyConfig drops a registrymodifications.xcu file into
|
||||
// writeSofficeProfileConfig drops a registrymodifications.xcu file into
|
||||
// userProfileDirPath/user/ that points soffice's UCB layer at proxyAddr
|
||||
// for both HTTP and HTTPS. proxyAddr must be a host:port pair.
|
||||
func writeSofficeProxyConfig(userProfileDirPath, proxyAddr string) error {
|
||||
// for both HTTP and HTTPS and blocks linked content from untrusted
|
||||
// locations. proxyAddr must be a host:port pair.
|
||||
func writeSofficeProfileConfig(userProfileDirPath, proxyAddr string) error {
|
||||
host, port, err := net.SplitHostPort(proxyAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("split proxy address %q: %w", proxyAddr, err)
|
||||
@@ -276,7 +290,7 @@ func writeSofficeProxyConfig(userProfileDirPath, proxyAddr string) error {
|
||||
return fmt.Errorf("create soffice user profile directory: %w", err)
|
||||
}
|
||||
|
||||
body := fmt.Sprintf(sofficeProxyConfigTmpl, host, port, host, port)
|
||||
body := fmt.Sprintf(sofficeProfileConfigTmpl, host, port, host, port)
|
||||
err = os.WriteFile(userDir+"/registrymodifications.xcu", []byte(body), 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write registrymodifications.xcu: %w", err)
|
||||
|
||||
@@ -280,11 +280,11 @@ func TestLibreOfficeProxy_StopIsIdempotent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSofficeProxyConfig(t *testing.T) {
|
||||
func TestWriteSofficeProfileConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
if err := writeSofficeProxyConfig(dir, "127.0.0.1:9876"); err != nil {
|
||||
t.Fatalf("writeSofficeProxyConfig: %v", err)
|
||||
if err := writeSofficeProfileConfig(dir, "127.0.0.1:9876"); err != nil {
|
||||
t.Fatalf("writeSofficeProfileConfig: %v", err)
|
||||
}
|
||||
|
||||
body, err := os.ReadFile(filepath.Join(dir, "user", "registrymodifications.xcu"))
|
||||
@@ -297,6 +297,9 @@ func TestWriteSofficeProxyConfig(t *testing.T) {
|
||||
`ooInetHTTPProxyName`, `<value>127.0.0.1</value>`,
|
||||
`ooInetHTTPProxyPort`, `<value>9876</value>`,
|
||||
`ooInetHTTPSProxyName`, `ooInetHTTPSProxyPort`,
|
||||
// Blocks linked content from untrusted locations, closing the
|
||||
// file:// local-read and direct-fetch vectors the proxy cannot see.
|
||||
`BlockUntrustedRefererLinks`, `<value>true</value>`,
|
||||
} {
|
||||
if !strings.Contains(string(body), want) {
|
||||
t.Errorf("xcu missing %q\nfull body:\n%s", want, body)
|
||||
@@ -304,8 +307,8 @@ func TestWriteSofficeProxyConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSofficeProxyConfig_InvalidAddr(t *testing.T) {
|
||||
err := writeSofficeProxyConfig(t.TempDir(), "not-a-host-port")
|
||||
func TestWriteSofficeProfileConfig_InvalidAddr(t *testing.T) {
|
||||
err := writeSofficeProfileConfig(t.TempDir(), "not-a-host-port")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for malformed proxy address")
|
||||
}
|
||||
|
||||
@@ -939,3 +939,28 @@ Feature: /forms/libreoffice/convert
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
Then there should be 1 PDF(s) in the response
|
||||
Then the "foo.pdf" PDF should have 1 page(s)
|
||||
|
||||
# An embedded image is stored inside the document, not linked, so blocking
|
||||
# untrusted linked content leaves it untouched. Guards against over-blocking.
|
||||
@libreoffice-linked-content
|
||||
Scenario: POST /forms/libreoffice/convert (Embedded Image Survives)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
|
||||
| files | testdata/libreoffice-embedded-image.fodt | file |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then there should be 1 PDF(s) in the response
|
||||
Then the "foo.pdf" PDF should have 1 image(s)
|
||||
|
||||
# An uploaded document always loads from an untrusted location, so soffice
|
||||
# refuses to resolve any content it links (absolute file:// path or external
|
||||
# URL). Closes the SSRF and local-file-read vector.
|
||||
@libreoffice-linked-content
|
||||
Scenario: POST /forms/libreoffice/convert (Linked External Resource Blocked)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
|
||||
| files | testdata/libreoffice-linked-external.fodt | file |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then there should be 1 PDF(s) in the response
|
||||
Then the "foo.pdf" PDF should have 0 image(s)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" office:version="1.2" office:mimetype="application/vnd.oasis.opendocument.text">
|
||||
<office:body><office:text>
|
||||
<text:p>An embedded image is stored in the document and must survive.</text:p>
|
||||
<text:p><draw:frame text:anchor-type="as-char" svg:width="3cm" svg:height="3cm"><draw:image><office:binary-data>iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC</office:binary-data></draw:image></draw:frame></text:p>
|
||||
</office:text></office:body>
|
||||
</office:document>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" office:version="1.2" office:mimetype="application/vnd.oasis.opendocument.text">
|
||||
<office:body><office:text>
|
||||
<text:p>An image linked by absolute path outside the document folder must not load.</text:p>
|
||||
<text:p><draw:frame text:anchor-type="as-char" svg:width="3cm" svg:height="3cm"><draw:image xlink:href="file:///usr/lib/libreoffice/share/gallery/backgrounds/giraffe.png" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/></draw:frame></text:p>
|
||||
</office:text></office:body>
|
||||
</office:document>
|
||||
Reference in New Issue
Block a user