# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import os from pathlib import Path from typing import Final import tornado.web from streamlit.logger import get_logger _LOGGER: Final = get_logger(__name__) # We agreed on these limitations for the initial release of static file sharing, # based on security concerns from the SiS and Community Cloud teams # The maximum possible size of single serving static file. MAX_APP_STATIC_FILE_SIZE = 200 * 1024 * 1024 # 200 MB # The list of file extensions that we serve with the corresponding Content-Type header. # All files with other extensions will be served with Content-Type: text/plain SAFE_APP_STATIC_FILE_EXTENSIONS = ( # Common image types: ".jpg", ".jpeg", ".png", ".gif", ".webp", # Common font types: ".otf", ".ttf", ".woff", ".woff2", # Other types: ".pdf", ".xml", ".json", ) class AppStaticFileHandler(tornado.web.StaticFileHandler): def initialize(self, path: str, default_filename: str | None = None) -> None: super().initialize(path, default_filename) def validate_absolute_path(self, root: str, absolute_path: str) -> str | None: full_path = os.path.abspath(absolute_path) ret_val = super().validate_absolute_path(root, absolute_path) if os.path.isdir(full_path): # we don't want to serve directories, and serve only files raise tornado.web.HTTPError(404) if os.path.commonpath([full_path, root]) != root: # Don't allow misbehaving clients to break out of the static files directory _LOGGER.warning( "Serving files outside of the static directory is not supported" ) raise tornado.web.HTTPError(404) if ( os.path.exists(full_path) and os.path.getsize(full_path) > MAX_APP_STATIC_FILE_SIZE ): raise tornado.web.HTTPError( 404, "File is too large, its size should not exceed " f"{MAX_APP_STATIC_FILE_SIZE} bytes", reason="File is too large", ) return ret_val def set_default_headers(self) -> None: # CORS protection is disabled because we need access to this endpoint # from the inner iframe. self.set_header("Access-Control-Allow-Origin", "*") def set_extra_headers(self, path: str) -> None: if Path(path).suffix not in SAFE_APP_STATIC_FILE_EXTENSIONS: self.set_header("Content-Type", "text/plain") self.set_header("X-Content-Type-Options", "nosniff")