Remove attacker-controlled error messages

Creates exceptions in the server code to be shared with the client via an
identifying exit code. These exceptions are then reconstructed in the
client.

Refs #456 but does not completely fix it. Unexpected exceptions and
progress descriptions are still passed in Containers.
This commit is contained in:
deeplow 2023-08-30 13:33:09 +01:00
parent 214ce9720d
commit b4c3e07d36
No known key found for this signature in database
GPG key ID: 577982871529A52A
6 changed files with 111 additions and 14 deletions

View file

@ -13,10 +13,11 @@ import os
import re import re
import shutil import shutil
import sys import sys
from typing import Dict, Optional from typing import Dict, List, Optional
import magic import magic
from . import errors
from .common import DangerzoneConverter, running_on_qubes from .common import DangerzoneConverter, running_on_qubes
@ -138,7 +139,7 @@ class DocumentToPixels(DangerzoneConverter):
# Validate MIME type # Validate MIME type
if mime_type not in conversions: if mime_type not in conversions:
raise ValueError("The document format is not supported") raise errors.DocFormatUnsupported()
# Temporary fix for the HWPX format # Temporary fix for the HWPX format
# Should be removed after new release of `file' (current release 5.44) # Should be removed after new release of `file' (current release 5.44)
@ -167,7 +168,7 @@ class DocumentToPixels(DangerzoneConverter):
# https://github.com/freedomofpress/dangerzone/issues/494 # https://github.com/freedomofpress/dangerzone/issues/494
# https://github.com/freedomofpress/dangerzone/issues/498 # https://github.com/freedomofpress/dangerzone/issues/498
if libreoffice_ext == "h2orestart.oxt" and running_on_qubes(): if libreoffice_ext == "h2orestart.oxt" and running_on_qubes():
raise ValueError("HWP / HWPX formats are not supported in Qubes") raise errors.DocFormatUnsupportedHWPQubes()
if libreoffice_ext: if libreoffice_ext:
await self.install_libreoffice_ext(libreoffice_ext) await self.install_libreoffice_ext(libreoffice_ext)
self.update_progress("Converting to PDF using LibreOffice") self.update_progress("Converting to PDF using LibreOffice")
@ -196,7 +197,7 @@ class DocumentToPixels(DangerzoneConverter):
# #
# https://github.com/freedomofpress/dangerzone/issues/494 # https://github.com/freedomofpress/dangerzone/issues/494
if not os.path.exists(pdf_filename): if not os.path.exists(pdf_filename):
raise ValueError("Conversion to PDF with LibreOffice failed") raise errors.LibreofficeFailure()
elif conversion["type"] == "convert": elif conversion["type"] == "convert":
self.update_progress("Converting to PDF using GraphicsMagick") self.update_progress("Converting to PDF using GraphicsMagick")
args = [ args = [
@ -216,7 +217,7 @@ class DocumentToPixels(DangerzoneConverter):
) )
pdf_filename = "/tmp/input_file.pdf" pdf_filename = "/tmp/input_file.pdf"
else: else:
raise ValueError( raise errors.InvalidGMConversion(
f"Invalid conversion type {conversion['type']} for MIME type {mime_type}" f"Invalid conversion type {conversion['type']} for MIME type {mime_type}"
) )
self.percentage += 3 self.percentage += 3
@ -236,7 +237,7 @@ class DocumentToPixels(DangerzoneConverter):
if search is not None: if search is not None:
num_pages: int = int(search.group(1)) num_pages: int = int(search.group(1))
else: else:
raise ValueError("Number of pages could not be extracted from PDF") raise errors.NoPageCountException()
# Get a more precise timeout, based on the number of pages # Get a more precise timeout, based on the number of pages
timeout = self.calculate_timeout(size, num_pages) timeout = self.calculate_timeout(size, num_pages)
@ -283,7 +284,7 @@ class DocumentToPixels(DangerzoneConverter):
# Read the header # Read the header
header = f.readline().decode().strip() header = f.readline().decode().strip()
if header != "P6": if header != "P6":
raise ValueError("Invalid PPM header") raise errors.PDFtoPPMInvalidHeader()
# Save the width and height # Save the width and height
dims = f.readline().decode().strip() dims = f.readline().decode().strip()
@ -296,7 +297,7 @@ class DocumentToPixels(DangerzoneConverter):
maxval = int(f.readline().decode().strip()) maxval = int(f.readline().decode().strip())
# Check that the depth is 8 # Check that the depth is 8
if maxval != 255: if maxval != 255:
raise ValueError("Invalid PPM depth") raise errors.PDFtoPPMInvalidDepth()
data = f.read() data = f.read()
@ -370,10 +371,11 @@ async def main() -> int:
try: try:
await converter.convert() await converter.convert()
error_code = 0 # Success! error_code = 0 # Success!
except (RuntimeError, TimeoutError, ValueError) as e: except errors.ConversionException as e: # Expected Errors
error_code = e.error_code
except (RuntimeError, TimeoutError, ValueError) as e: # Unexpected Errors
converter.update_progress(str(e), error=True) converter.update_progress(str(e), error=True)
error_code = 1 error_code = 1
if not running_on_qubes(): if not running_on_qubes():
# Write debug information (containers version) # Write debug information (containers version)
with open("/tmp/dangerzone/captured_output.txt", "wb") as container_log: with open("/tmp/dangerzone/captured_output.txt", "wb") as container_log:

View file

@ -6,6 +6,7 @@ import tempfile
from pathlib import Path from pathlib import Path
from typing import Optional, TextIO from typing import Optional, TextIO
from . import errors
from .doc_to_pixels import DocumentToPixels from .doc_to_pixels import DocumentToPixels
@ -67,8 +68,13 @@ async def main() -> None:
with open("/tmp/input_file", "wb") as f: with open("/tmp/input_file", "wb") as f:
f.write(data) f.write(data)
converter = DocumentToPixels() try:
await converter.convert() converter = DocumentToPixels()
await converter.convert()
except errors.ConversionException as e:
sys.exit(e.error_code)
except (RuntimeError, TimeoutError, ValueError) as e:
sys.exit(1)
num_pages = len(list(out_dir.glob("*.rgb"))) num_pages = len(list(out_dir.glob("*.rgb")))
await write_int(num_pages) await write_int(num_pages)

View file

@ -0,0 +1,73 @@
from typing import List, Optional, Type
class ConversionException(Exception):
error_message = "Unspecified error"
error_code = -1
def __init__(self, error_message: Optional[str] = None) -> None:
if error_message:
self.error_message = error_message
super().__init__(self.error_message)
@classmethod
def get_subclasses(cls) -> List[Type["ConversionException"]]:
subclasses = [cls]
for subclass in cls.__subclasses__():
subclasses += subclass.get_subclasses()
return subclasses
class DocFormatUnsupported(ConversionException):
error_code = 10
error_message = "The document format is not supported"
class DocFormatUnsupportedHWPQubes(DocFormatUnsupported):
error_code = 16
error_message = "HWP / HWPX formats are not supported in Qubes"
class LibreofficeFailure(ConversionException):
error_code = 20
error_message = "Conversion to PDF with LibreOffice failed"
class InvalidGMConversion(ConversionException):
error_code = 30
error_message = "Invalid conversion (Graphics Magic)"
def __init__(self, error_message: str) -> None:
super(error_message)
class PagesException(ConversionException):
error_code = 40
class NoPageCountException(PagesException):
error_code = 41
error_message = "Number of pages could not be extracted from PDF"
class PDFtoPPMException(ConversionException):
error_code = 50
error_message = "Error converting PDF to Pixels (pdftoppm)"
class PDFtoPPMInvalidHeader(PDFtoPPMException):
error_code = 51
error_message = "Error converting PDF to Pixels (Invalid PPM header)"
class PDFtoPPMInvalidDepth(PDFtoPPMException):
error_code = 52
error_message = "Error converting PDF to Pixels (Invalid PPM depth)"
def exception_from_error_code(error_code: int) -> Optional[ConversionException]:
"""returns the conversion exception corresponding to the error code"""
for cls in ConversionException.get_subclasses():
if cls.error_code == error_code:
return cls()
raise ValueError(f"Unknown error code '{error_code}'")

View file

@ -5,6 +5,7 @@ from typing import Callable, Optional
from colorama import Fore, Style from colorama import Fore, Style
from ..conversion.errors import ConversionException
from ..document import Document from ..document import Document
from ..util import replace_control_chars from ..util import replace_control_chars
@ -36,7 +37,10 @@ class IsolationProvider(ABC):
document.mark_as_converting() document.mark_as_converting()
try: try:
success = self._convert(document, ocr_lang) success = self._convert(document, ocr_lang)
except Exception: except ConversionException as e:
success = False
self.print_progress_trusted(document, True, e.error_message, 0)
except Exception as e:
success = False success = False
log.exception( log.exception(
f"An exception occurred while converting document '{document.id}'" f"An exception occurred while converting document '{document.id}'"

View file

@ -11,6 +11,7 @@ import sys
import tempfile import tempfile
from typing import Any, Callable, List, Optional, Tuple from typing import Any, Callable, List, Optional, Tuple
from ..conversion.errors import exception_from_error_code
from ..document import Document from ..document import Document
from ..util import ( from ..util import (
get_resource_path, get_resource_path,
@ -305,6 +306,9 @@ class Container(IsolationProvider):
if ret != 0: if ret != 0:
log.error("documents-to-pixels failed") log.error("documents-to-pixels failed")
# XXX Reconstruct exception from error code
raise exception_from_error_code(ret) # type: ignore [misc]
else: else:
# TODO: validate convert to pixels output # TODO: validate convert to pixels output

View file

@ -20,6 +20,7 @@ from .base import IsolationProvider
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
from ..conversion.common import running_on_qubes from ..conversion.common import running_on_qubes
from ..conversion.errors import exception_from_error_code
from ..conversion.pixels_to_pdf import PixelsToPDF from ..conversion.pixels_to_pdf import PixelsToPDF
from ..util import get_resource_path, get_subprocess_startupinfo, get_tmp_dir from ..util import get_resource_path, get_subprocess_startupinfo, get_tmp_dir
from .base import MAX_CONVERSION_LOG_CHARS from .base import MAX_CONVERSION_LOG_CHARS
@ -38,6 +39,8 @@ def read_bytes(p: subprocess.Popen, buff_size: int) -> bytes:
def read_int(p: subprocess.Popen) -> int: def read_int(p: subprocess.Popen) -> int:
"""Read 2 bytes from stdout, and decode them as int.""" """Read 2 bytes from stdout, and decode them as int."""
untrusted_int = p.stdout.read(2) # type: ignore [union-attr] untrusted_int = p.stdout.read(2) # type: ignore [union-attr]
if untrusted_int == b"":
raise ValueError("Nothing read from stdout (expected an integer)")
return int.from_bytes(untrusted_int, signed=False) return int.from_bytes(untrusted_int, signed=False)
@ -104,7 +107,12 @@ class Qubes(IsolationProvider):
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
) )
n_pages = read_int(p) try:
n_pages = read_int(p)
except ValueError:
error_code = p.wait()
# XXX Reconstruct exception from error code
raise exception_from_error_code(error_code) # type: ignore [misc]
if n_pages == 0: if n_pages == 0:
# FIXME: Fail loudly in that case # FIXME: Fail loudly in that case
return False return False