mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	uploads: Set Content-Type and -Disposition from Django for local files.
Similar to the previous commit, Django was responsible for setting the Content-Disposition based on the filename, whereas the Content-Type was set by nginx based on the filename. This difference is not exploitable, as even if they somehow disagreed with Django's expected Content-Type, nginx will only ever respond with Content-Types found in `uploads.types` -- none of which are unsafe for user-supplied content. However, for consistency, have Django provide both Content-Type and Content-Disposition headers.
This commit is contained in:
		@@ -50,7 +50,9 @@ location /internal/local/uploads {
 | 
			
		||||
    internal;
 | 
			
		||||
    include /etc/nginx/zulip-include/headers;
 | 
			
		||||
    add_header Content-Security-Policy "default-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self'; object-src 'self'; plugin-types application/pdf;";
 | 
			
		||||
    include /etc/nginx/zulip-include/uploads.types;
 | 
			
		||||
 | 
			
		||||
    # Django handles setting Content-Type, Content-Disposition, and Cache-Control.
 | 
			
		||||
 | 
			
		||||
    alias /home/zulip/uploads/files;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,7 @@ def patch_disposition_header(response: HttpResponse, url: str, is_attachment: bo
 | 
			
		||||
    response.headers["Content-Disposition"] = f"{disposition}; {file_expr}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def internal_nginx_redirect(internal_path: str) -> HttpResponse:
 | 
			
		||||
def internal_nginx_redirect(internal_path: str, content_type: Optional[str] = None) -> HttpResponse:
 | 
			
		||||
    # The following headers from this initial response are
 | 
			
		||||
    # _preserved_, if present, and sent unmodified to the client;
 | 
			
		||||
    # all other headers are overridden by the redirected URL:
 | 
			
		||||
@@ -78,12 +78,15 @@ def internal_nginx_redirect(internal_path: str) -> HttpResponse:
 | 
			
		||||
    #  - Set-Cookie
 | 
			
		||||
    #  - Cache-Control
 | 
			
		||||
    #  - Expires
 | 
			
		||||
    # As such, we unset the Content-type header to allow nginx to set
 | 
			
		||||
    # it from the static file; the caller can set Content-Disposition
 | 
			
		||||
    # and Cache-Control on this response as they desire, and the
 | 
			
		||||
    # client will see those values.
 | 
			
		||||
    response = HttpResponse()
 | 
			
		||||
    # As such, we default to unsetting the Content-type header to
 | 
			
		||||
    # allow nginx to set it from the static file; the caller can set
 | 
			
		||||
    # Content-Disposition and Cache-Control on this response as they
 | 
			
		||||
    # desire, and the client will see those values.  In some cases
 | 
			
		||||
    # (local files) we do wish to control the Content-Type, so also
 | 
			
		||||
    # support setting it explicitly.
 | 
			
		||||
    response = HttpResponse(content_type=content_type)
 | 
			
		||||
    response["X-Accel-Redirect"] = internal_path
 | 
			
		||||
    if content_type is None:
 | 
			
		||||
        del response["Content-Type"]
 | 
			
		||||
    return response
 | 
			
		||||
 | 
			
		||||
@@ -133,15 +136,23 @@ def serve_local(
 | 
			
		||||
 | 
			
		||||
    if settings.DEVELOPMENT:
 | 
			
		||||
        # In development, we do not have the nginx server to offload
 | 
			
		||||
        # the response to; serve it directly ourselves.
 | 
			
		||||
        # FileResponse handles setting Content-Disposition, etc.
 | 
			
		||||
        # the response to; serve it directly ourselves.  FileResponse
 | 
			
		||||
        # handles setting Content-Type, Content-Disposition, etc.
 | 
			
		||||
        response: HttpResponseBase = FileResponse(
 | 
			
		||||
            open(local_path, "rb"), as_attachment=download  # noqa: SIM115
 | 
			
		||||
        )
 | 
			
		||||
        patch_cache_control(response, private=True, immutable=True)
 | 
			
		||||
        return response
 | 
			
		||||
 | 
			
		||||
    response = internal_nginx_redirect(quote(f"/internal/local/uploads/{path_id}"))
 | 
			
		||||
    # For local responses, we are in charge of generating both
 | 
			
		||||
    # Content-Type and Content-Disposition headers; unlike with S3
 | 
			
		||||
    # storage, the Content-Type is not stored with the file in any
 | 
			
		||||
    # way, so Django makes the determination of it, and thus as well
 | 
			
		||||
    # if that type is safe to have a Content-Disposition of "inline".
 | 
			
		||||
    # nginx respects the values we send.
 | 
			
		||||
    response = internal_nginx_redirect(
 | 
			
		||||
        quote(f"/internal/local/uploads/{path_id}"), content_type=mimetype
 | 
			
		||||
    )
 | 
			
		||||
    patch_disposition_header(response, local_path, download)
 | 
			
		||||
    patch_cache_control(response, private=True, immutable=True)
 | 
			
		||||
    return response
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user