Improve security of legacy Jam.py v5 applications

7 views
Skip to first unread message

Moh. Sarip Hidayat

unread,
Jul 15, 2026, 12:00:27 AM (3 days ago) Jul 15
to Jam.py Users Mailing List

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

You can then run your application using gunicorn like so:
gunicorn --bind 0.0.0.0:8080 --workers 2 --threads 2 --log-level debug --capture-output wsgi:application

How it works:
  • Authenticated 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.

One quick note:

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!

Thank you.

Best Regards,
Moh. Sarip Hidayat
Visit my website at musahi0128.github.io

Dean D. Babic

unread,
Jul 15, 2026, 2:50:36 AM (2 days ago) Jul 15
to Jam.py Users Mailing List
Nice man! Thanks.
Re large amount of code, the option "Compressed JS, CSS files" can be used.

Reply all
Reply to author
Forward
0 new messages