Repository: https://github.com/horilla/horilla-hr.git Vulnerable target: 1.5.0 61bd5173220d19925ad8220db9152a75c881ea73 Fixed target: 1.6.0 b3bd29d15819cbece45c58e6268ddd0614e387d6 Source fix commit: b6eaec1386d8b8741a42fe7c78f318f073375791 Latest inspected ref: origin/master 11c4e3a2596c58f2381bda4c6bbc319a4430b097 --- SECURITY.md excerpt --- # Horilla Open Source Project - Security Guidelines Thank you for your interest in contributing to the Horilla open-source project. We take security seriously and value the community's efforts in helping us identify and address security vulnerabilities. This document outlines the security guidelines for reporting and addressing security issues within the Horilla project. ## Reporting Security Issues If you discover a security vulnerability in the Horilla project, please follow these steps to report it: 1. **Do Not** create a public GitHub issue for the security vulnerability. 2. **Do Not** disclose the vulnerability details publicly until the issue has been resolved. 3. Notify the project maintainers by sending an email to `info@horilla.com`. Include a detailed description of the vulnerability, including the potential impact and any relevant technical details. 4. A project maintainer will respond to your email within 72 hours to acknowledge the report and begin the investigation process. 5. Once the security vulnerability has been verified and validated, we will work on a fix. 6. We will collaborate with you to coordinate the release of the fix and any necessary announcements. ## Vulnerability Handling Horilla project follows these principles when handling security vulnerabilities: - **Swift Response:** We strive to respond to and address security vulnerabilities as quickly as possible. The time to resolution may vary depending on the complexity of the issue, but we will keep you updated on our progress. - **Coordinated Disclosure:** We follow a coordinated disclosure process, meaning we work with the reporter to ensure that the vulnerability is disclosed responsibly. This may involve a joint announcement or other measures to protect users until a fix is available. - **Public Disclosure:** Once the vulnerability is fixed and released, we will publicly acknowledge your contribution to the security of the project, unless you prefer to remain anonymous. ## Security Best Practices For contributors to the Horilla project, we recommend the following security best practices: - **Code Review:** Perform thorough code reviews to identify and address security issues before they are merged into the project's codebase. - **Secure Dependencies:** Regularly update and monitor the project's dependencies for security vulnerabilities. Use trusted and well-maintained packages. - **Authentication and Authorization:** Implement strong authentication and authorization mechanisms to prevent unauthorized access to sensitive resources. - **Data Validation:** Validate and sanitize all user inputs to prevent common security vulnerabilities such as SQL injection, Cross-Site Scripting (XSS), and more. - **Secure Configuration:** Store sensitive configuration and credentials securely, and avoid hardcoding secrets in your codebase. - **Sensitive Data Handling:** Handle sensitive data, such as passwords and tokens, with care. Store them securely using encryption and follow industry best practices for data protection. - **Regular Security Audits:** Conduct periodic security audits and assessments of the project's codebase and infrastructure. ## Disclaimer The Horilla project and its maintainers assume no liability for any security vulnerabilities reported or discovered. We do, however, greatly appreciate your help in keeping the project secure. ## Contact If you have any questions or concerns about the security guidelines or the project's security practices, please contact us at `info@horilla.com`. --- source fix diff for base/views.py --- commit b6eaec1386d8b8741a42fe7c78f318f073375791 Author: Horilla Date: Wed May 20 11:21:40 2026 +0530 [FIX] BASE: SSTI in mail preview exposing password hashes via template body --- base/views.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/base/views.py b/base/views.py index b28df295..63d0c859 100644 --- a/base/views.py +++ b/base/views.py @@ -7506,18 +7506,15 @@ def is_jwt_token_valid(auth_header): def protected_media(request, path): - public_pages = [ - "/login", - "/forgot-password", - "/change-username", - "/change-password", - "/employee-reset-password", - "/recruitment/candidate-survey", - "/recruitment/open-recruitments", - "/recruitment/candidate-self-status-tracking", - ] - - exempted_folders = ["base/icon/"] + # Media-path prefixes that are safe to serve without authentication. + # These are assets rendered on genuinely-public pages (login screen, + # open recruitments, candidate self-tracking) and contain no sensitive + # employee data. Do NOT add prefixes here without security review. + public_media_prefixes = ( + "base/icon/", + "base/company/icon/", + "recruitment/candidate/profile/", + ) # Prevent path traversal try: @@ -7529,15 +7526,10 @@ def protected_media(request, path): if not os.path.exists(media_path) or not os.path.isfile(media_path): raise Http404("File not found") - referer_path = urlparse(request.META.get("HTTP_REFERER", "")).path + is_public_asset = any(path.startswith(prefix) for prefix in public_media_prefixes) - # Try Bearer token auth - jwt_user = is_jwt_token_valid(request.META.get("HTTP_AUTHORIZATION", "")) - - # Access control logic - if referer_path not in public_pages and not any( - path.startswith(folder) for folder in exempted_folders - ): + if not is_public_asset: + jwt_user = is_jwt_token_valid(request.META.get("HTTP_AUTHORIZATION", "")) if not request.user.is_authenticated and not jwt_user: messages.error( request, --- vulnerable protected_media excerpt --- def protected_media(request, path): public_pages = [ "/login", "/forgot-password", "/change-username", "/change-password", "/employee-reset-password", "/recruitment/candidate-survey", "/recruitment/open-recruitments", "/recruitment/candidate-self-status-tracking", ] exempted_folders = ["base/icon/"] media_path = os.path.join(settings.MEDIA_ROOT, path) if not os.path.exists(media_path): raise Http404("File not found") referer_path = urlparse(request.META.get("HTTP_REFERER", "")).path # Try Bearer token auth jwt_user = is_jwt_token_valid(request.META.get("HTTP_AUTHORIZATION", "")) # Access control logic if referer_path not in public_pages and not any( path.startswith(f) for f in exempted_folders ): if not request.user.is_authenticated and not jwt_user: messages.error( request, "You must be logged in or provide a valid token to access this file.", ) return redirect("login") return FileResponse(open(media_path, "rb")) --- fixed protected_media excerpt --- def protected_media(request, path): # Media-path prefixes that are safe to serve without authentication. # These are assets rendered on genuinely-public pages (login screen, # open recruitments, candidate self-tracking) and contain no sensitive # employee data. Do NOT add prefixes here without security review. public_media_prefixes = ( "base/icon/", "base/company/icon/", "recruitment/candidate/profile/", ) # Prevent path traversal try: media_path = safe_join(settings.MEDIA_ROOT, path) except Exception: # safe_join raises ValueError if traversal detected raise Http404("Invalid file path") if not os.path.exists(media_path) or not os.path.isfile(media_path): raise Http404("File not found") is_public_asset = any(path.startswith(prefix) for prefix in public_media_prefixes) if not is_public_asset: jwt_user = is_jwt_token_valid(request.META.get("HTTP_AUTHORIZATION", "")) if not request.user.is_authenticated and not jwt_user: messages.error( request, "You must be logged in or provide a valid token to access this file.", ) return redirect("login") return FileResponse(open(media_path, "rb")) --- latest inspected protected_media excerpt --- def protected_media(request, path): # Media-path prefixes that are safe to serve without authentication. # These are assets rendered on genuinely-public pages (login screen, # open recruitments, candidate self-tracking) and contain no sensitive # employee data. Do NOT add prefixes here without security review. public_media_prefixes = ( "base/icon/", "base/company/icon/", "recruitment/candidate/profile/", ) # Prevent path traversal try: media_path = safe_join(settings.MEDIA_ROOT, path) except Exception: # safe_join raises ValueError if traversal detected raise Http404("Invalid file path") if not os.path.exists(media_path) or not os.path.isfile(media_path): raise Http404("File not found") is_public_asset = any(path.startswith(prefix) for prefix in public_media_prefixes) if not is_public_asset: jwt_user = is_jwt_token_valid(request.META.get("HTTP_AUTHORIZATION", "")) if not request.user.is_authenticated and not jwt_user: messages.error( request, "You must be logged in or provide a valid token to access this file.", ) return redirect("login") import mimetypes filename = os.path.basename(media_path) mime_type, _ = mimetypes.guess_type(media_path) if not mime_type: mime_type = "application/octet-stream" # Types that browsers will render as active content — force download so # uploaded files can never execute scripts in the application's origin. FORCE_DOWNLOAD_TYPES = { "text/html", "application/xhtml+xml", "image/svg+xml", "text/xml", "application/xml", "application/javascript", "text/javascript", } response = FileResponse(open(media_path, "rb"), content_type=mime_type) response["X-Content-Type-Options"] = "nosniff" if mime_type in FORCE_DOWNLOAD_TYPES or mime_type == "application/octet-stream": response["Content-Disposition"] = f'attachment; filename="{filename}"' else: response["Content-Disposition"] = f'inline; filename="{filename}"' return response