Hi everyone,
I would like to share a security workaround for those who are still running legacy Jam.py v5 applications.
As some of you might know, a notable security loophole in v5 allows anyone who knows the exact path to your /js/{app_name}.js file to fetch and read your client module's business logic—entirely bypassing authentication.
If your client-side code contains sensitive or proprietary logic, this is a significant risk.
Thankfully, this issue is fully addressed in Jam.py v7, which is why migrating to v7 is highly recommended for both existing and new projects.
However, if you have a project that is stuck on v5 and cannot be easily upgraded right now, you can patch this vulnerability directly via your wsgi.py file.
Here is a modified version of wsgi.py that intercepts requests to protected paths:
import logging from jam.wsgi import create_application, JamRequest # Get logger - use gunicorn.error which is guaranteed to be set up logger = logging.getLogger('gunicorn.error') # Create the base Jam.py application _base_app = create_application(__file__) # Add any static paths here that must require authentication. PROTECTED_PATHS = [ # "/js/app.js", # "/js/app.min.js", # "/css/project.css" ] _APP_NAME = None def get_jam_app(): """Return jam.wsgi.App from wrapped WSGI callable, or None if unavailable.""" closure = getattr(_base_app, '__closure__', None) or () if not closure: return None try: shared = closure[0].cell_contents return getattr(shared, 'app', None) except Exception as e: logger.error(f'Unable to extract Jam app from _base_app: {e}') return None def get_app_name(): """Return the name of the Jam.py application.""" global _APP_NAME if _APP_NAME: return _APP_NAME app = get_jam_app() if app is None: return None d = app.admin.task.sys_tasks.copy() d.open(where={'id': 1}) if d.record_count: _APP_NAME = d.f_item_name.value return _APP_NAME return None def get_protected_paths(): """Return static paths requiring authentication.""" paths = list(PROTECTED_PATHS) app_name = get_app_name() if app_name: defaults = [ f"/js/{app_name}.js", f"/js/{app_name}.min.js", '/css/project.css' ] for p in defaults: if p not in paths: paths.append(p) return paths def is_request_session_valid(environ): """Use Jam.py native session validation logic for current request.""" try: app = get_jam_app() if app is None: logger.warning('Unable to validate session: Jam app object is unavailable') return False task = app.task if task is None: logger.warning('Unable to validate session: Jam task is unavailable') return False request = JamRequest(environ) request.task = task is_valid = bool(app.check_session(request, task)) logger.debug(f'Jam native session check result: {is_valid}') return is_valid except Exception as e: logger.error(f'Error validating session with Jam native checker: {e}') return False def protected_application(environ, start_response): """ Middleware to protect js/app.js from unauthenticated users. Returns 401 Unauthorized if user has no valid app_session. """ path = environ.get('PATH_INFO', '') protected_paths = get_protected_paths() # Protect configured static paths - only serve to authenticated users if path in protected_paths: logger.debug(f'Secure handling for protected path: {path}') remote_addr = environ.get('REMOTE_ADDR', 'unknown') # Validate session using Jam.py native logic (check_session -> valid_session). if not is_request_session_valid(environ): logger.warning(f'Blocked unauthorized access to {path} from {remote_addr} (session invalid)') start_response('401 Unauthorized', [ ('Content-Type', 'text/plain'), ('Cache-Control', 'no-cache, no-store, must-revalidate') ]) return [b'Access denied: Please log in first'] logger.debug(f'Allowed access to {path} from {remote_addr}') # Pass all requests through to the base Jam.py application return _base_app(environ, start_response)
# Use the protected application as the entry point application = protected_application
gunicorn --bind 0.0.0.0:8080 --workers 2 --threads 2 --log-level debug --capture-output wsgi:applicationAuthenticated Clients: The script verifies the request. If the client is correctly authenticated, access to the protected paths is allowed normally.
Unauthenticated Requests: If an unauthenticated user attempts to access the path, the request is blocked, and the server returns a 401 Unauthorized response.
If your primary application module contains a very large amount of client-side code, you might experience a minor hiccup immediately after logging in.
If the application doesn't load fully on the first try, simply refresh the page, and everything should function normally from there.
Hope this helps anyone maintaining older v5 apps!