VYPR
Critical severity9.6NVD Advisory· Published Apr 10, 2026· Updated Apr 16, 2026

CVE-2026-1115

CVE-2026-1115

Description

A Stored Cross-Site Scripting (XSS) vulnerability was identified in the social feature of parisneo/lollms, affecting the latest version prior to 2.2.0. The vulnerability exists in the create_post function within backend/routers/social/__init__.py, where user-provided content is directly assigned to the DBPost model without sanitization. This allows attackers to inject and store malicious JavaScript, which is executed in the browsers of users viewing the Home Feed, including administrators. This can lead to account takeover, session hijacking, and wormable attacks. The issue is resolved in version 2.2.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
lollmsPyPI
< 2.2.02.2.0

Affected products

1
  • cpe:2.3:a:lollms:lollms:*:*:*:*:*:*:*:*
    Range: <=2.1.0

Patches

1
9767b882dbc8

feat(social,dm): add input sanitization with bleach

https://github.com/parisneo/lollmsSaifeddine ALOUIJan 17, 2026via ghsa
4 files changed · +183 7
  • backend/routers/social/dm.py+33 3 modified
    @@ -11,6 +11,7 @@
     from werkzeug.utils import secure_filename
     from datetime import datetime
     from pydantic import BaseModel, Field
    +import bleach
     
     from backend.db import get_db
     from backend.db.models.user import User as DBUser
    @@ -23,6 +24,26 @@
     
     dm_router = APIRouter(prefix="/api/dm", tags=["Direct Messaging"])
     
    +# --- Security: Sanitization Config (Shared config could be moved to utils, but keeping localized for now) ---
    +ALLOWED_TAGS = [
    +    'p', 'b', 'i', 'u', 'em', 'strong', 'a', 'br', 'ul', 'ol', 'li', 
    +    'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
    +    'table', 'thead', 'tbody', 'tr', 'th', 'td', 'strike', 'hr', 'span', 'div'
    +]
    +ALLOWED_ATTRS = {
    +    'a': ['href', 'title', 'target', 'rel'],
    +    'img': ['src', 'alt', 'title', 'width', 'height'],
    +    'span': ['class'],
    +    'div': ['class'],
    +    'code': ['class'],
    +    'pre': ['class']
    +}
    +
    +def sanitize_content(content: str) -> str:
    +    if not content:
    +        return content
    +    return bleach.clean(content, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS, strip=True)
    +
     class BroadcastDMRequest(BaseModel):
         content: str = Field(..., min_length=1)
     
    @@ -33,6 +54,9 @@ def _broadcast_dm_task(task: Task, sender_id: int, content: str):
             if not sender:
                 raise Exception("Sender not found")
             
    +        # Sanitize broadcast content
    +        clean_content = sanitize_content(content)
    +
             # Get all active users except sender
             users = db.query(DBUser).filter(DBUser.id != sender_id, DBUser.is_active == True).all()
             total = len(users)
    @@ -47,7 +71,7 @@ def _broadcast_dm_task(task: Task, sender_id: int, content: str):
                 new_message = DBDirectMessage(
                     sender_id=sender_id,
                     receiver_id=user.id,
    -                content=content
    +                content=clean_content
                 )
                 db.add(new_message)
                 # Commit frequently to ensure messages are saved
    @@ -89,7 +113,10 @@ async def create_group_conversation(
         current_user: UserAuthDetails = Depends(get_current_active_user),
         db: Session = Depends(get_db)
     ):
    -    new_conv = DBConversation(name=payload.name, is_group=1)
    +    # Sanitize group name
    +    clean_name = sanitize_content(payload.name)
    +    
    +    new_conv = DBConversation(name=clean_name, is_group=1)
         db.add(new_conv)
         db.commit()
         db.refresh(new_conv)
    @@ -198,6 +225,9 @@ async def send_direct_message(
         if not receiver_user_id and not conversation_id:
             raise HTTPException(status_code=400, detail="Either receiverUserId or conversationId must be provided.")
     
    +    # Sanitize DM content
    +    clean_content = sanitize_content(content)
    +
         image_paths = []
         if files:
             dm_assets_path = get_user_dm_assets_path(current_user.username)
    @@ -216,7 +246,7 @@ async def send_direct_message(
     
         new_message = DBDirectMessage(
             sender_id=current_user.id,
    -        content=content,
    +        content=clean_content,
             image_references=image_paths if image_paths else None
         )
     
    
  • backend/routers/social/__init__.py+42 4 modified
    @@ -5,6 +5,7 @@
     from fastapi import APIRouter, Depends, HTTPException, status
     from ascii_colors import trace_exception
     import re
    +import bleach
     from backend.settings import settings
     from backend.task_manager import task_manager
     from backend.tasks.social_tasks import _respond_to_mention_task, _moderate_content_task
    @@ -32,6 +33,32 @@
     )
     social_router.include_router(mentions_router, prefix="/mentions")
     
    +# --- Security: Sanitization Config ---
    +ALLOWED_TAGS = [
    +    'p', 'b', 'i', 'u', 'em', 'strong', 'a', 'br', 'ul', 'ol', 'li', 
    +    'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
    +    'table', 'thead', 'tbody', 'tr', 'th', 'td', 'strike', 'hr', 'span', 'div'
    +]
    +
    +ALLOWED_ATTRS = {
    +    'a': ['href', 'title', 'target', 'rel'],
    +    'img': ['src', 'alt', 'title', 'width', 'height'],
    +    'span': ['class'],
    +    'div': ['class'],
    +    'code': ['class'],
    +    'pre': ['class']
    +}
    +
    +def sanitize_content(content: str) -> str:
    +    """
    +    Sanitizes user input to prevent XSS while allowing basic formatting.
    +    """
    +    if not content:
    +        return content
    +    # bleach.clean will strip or escape tags not in ALLOWED_TAGS
    +    # and strip attributes not in ALLOWED_ATTRS.
    +    return bleach.clean(content, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS, strip=True)
    +
     # --- Helpers ---
     def get_post_public(db: Session, post: DBPost, current_user_id: int) -> PostPublic:
         # Logic to convert DBPost to PostPublic, including like counts and status
    @@ -149,9 +176,12 @@ def create_post(
         moderation_enabled = settings.get("ai_bot_moderation_enabled", False)
         initial_status = "pending" if moderation_enabled else "validated"
     
    +    # Sanitize content to prevent Stored XSS
    +    clean_content = sanitize_content(post_data.content)
    +
         new_post = DBPost(
             author_id=current_user.id,
    -        content=post_data.content,
    +        content=clean_content,
             visibility=post_data.visibility,
             media=post_data.media,
             moderation_status=initial_status
    @@ -162,7 +192,7 @@ def create_post(
         
         # --- NEW: Check for @lollms mention ---
         if settings.get("ai_bot_enabled", False):
    -        if re.search(r'\B@lollms\b', post_data.content, re.IGNORECASE):
    +        if re.search(r'\B@lollms\b', clean_content, re.IGNORECASE):
                 task_manager.submit_task(
                     name=f"AI Bot responding to post by {current_user.username}",
                     target=_respond_to_mention_task,
    @@ -241,6 +271,11 @@ def update_post(
             raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You can only edit your own posts.")
     
         update_data = post_data.model_dump(exclude_unset=True)
    +    
    +    # Sanitize content update
    +    if 'content' in update_data and update_data['content']:
    +        update_data['content'] = sanitize_content(update_data['content'])
    +
         for key, value in update_data.items():
             setattr(post, key, value)
         
    @@ -439,10 +474,13 @@ def add_comment_to_post(
         moderation_enabled = settings.get("ai_bot_moderation_enabled", False)
         initial_status = "pending" if moderation_enabled else "validated"
     
    +    # Sanitize comment content
    +    clean_content = sanitize_content(comment_data.content)
    +
         new_comment = DBComment(
             post_id=post_id,
             author_id=current_user.id,
    -        content=comment_data.content,
    +        content=clean_content,
             moderation_status=initial_status
         )
         db.add(new_comment)
    @@ -452,7 +490,7 @@ def add_comment_to_post(
         # Mention Response
         if settings.get("ai_bot_enabled", False):
             if current_user.username != 'lollms':
    -            is_explicit_mention = re.search(r'\B@lollms\b', comment_data.content, re.IGNORECASE)
    +            is_explicit_mention = re.search(r'\B@lollms\b', clean_content, re.IGNORECASE)
                 post_author_username = post.author.username if post.author else db.query(DBUser.username).filter(DBUser.id == post.author_id).scalar()
                 is_bot_post = (post_author_username == 'lollms')
                 was_mentioned_in_post = re.search(r'\B@lollms\b', post.content, re.IGNORECASE)
    
  • CHANGELOG.md+4 0 modified
    @@ -44,6 +44,10 @@ All notable changes to the LoLLMs Platform will be documented in this file.
     
     ## [2026-01-17 14:28]
     
    +- feat(social,dm): add input sanitization with bleach
    +
    +## [2026-01-17 14:28]
    +
     - feat(social): add missing imports and update comments
     
     ## [2026-01-17 12:55]
    
  • scripts/sanitize_existing_content.py+104 0 added
    @@ -0,0 +1,104 @@
    +import sys
    +import os
    +from pathlib import Path
    +import bleach
    +
    +# Add the project root to python path so we can import backend modules
    +project_root = Path(__file__).resolve().parent.parent
    +sys.path.append(str(project_root))
    +
    +from backend.db import get_db, session as db_session_module
    +from backend.db.models.social import Post, Comment
    +from backend.db.models.dm import DirectMessage, Conversation
    +
    +# Configuration matching the API fix
    +ALLOWED_TAGS = [
    +    'p', 'b', 'i', 'u', 'em', 'strong', 'a', 'br', 'ul', 'ol', 'li', 
    +    'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
    +    'table', 'thead', 'tbody', 'tr', 'th', 'td', 'strike', 'hr', 'span', 'div'
    +]
    +
    +ALLOWED_ATTRS = {
    +    'a': ['href', 'title', 'target', 'rel'],
    +    'img': ['src', 'alt', 'title', 'width', 'height'],
    +    'span': ['class'],
    +    'div': ['class'],
    +    'code': ['class'],
    +    'pre': ['class']
    +}
    +
    +def sanitize_content(content: str) -> str:
    +    if not content:
    +        return content
    +    return bleach.clean(content, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS, strip=True)
    +
    +def run_sanitization():
    +    print("--- Starting Database Sanitization ---")
    +    
    +    # Initialize DB
    +    from backend.config import APP_DB_URL
    +    from backend.db import init_database
    +    init_database(APP_DB_URL)
    +    
    +    db = db_session_module.SessionLocal()
    +    
    +    try:
    +        # 1. Sanitize Posts
    +        print("Scanning Posts...")
    +        posts = db.query(Post).all()
    +        count = 0
    +        for post in posts:
    +            if post.content:
    +                clean = sanitize_content(post.content)
    +                if clean != post.content:
    +                    post.content = clean
    +                    count += 1
    +        print(f"  - Sanitized {count} Posts.")
    +
    +        # 2. Sanitize Comments
    +        print("Scanning Comments...")
    +        comments = db.query(Comment).all()
    +        count = 0
    +        for comment in comments:
    +            if comment.content:
    +                clean = sanitize_content(comment.content)
    +                if clean != comment.content:
    +                    comment.content = clean
    +                    count += 1
    +        print(f"  - Sanitized {count} Comments.")
    +
    +        # 3. Sanitize Direct Messages
    +        print("Scanning Direct Messages...")
    +        dms = db.query(DirectMessage).all()
    +        count = 0
    +        for dm in dms:
    +            if dm.content:
    +                clean = sanitize_content(dm.content)
    +                if clean != dm.content:
    +                    dm.content = clean
    +                    count += 1
    +        print(f"  - Sanitized {count} Direct Messages.")
    +
    +        # 4. Sanitize Group Names
    +        print("Scanning Conversation Names...")
    +        convs = db.query(Conversation).all()
    +        count = 0
    +        for conv in convs:
    +            if conv.name:
    +                clean = sanitize_content(conv.name)
    +                if clean != conv.name:
    +                    conv.name = clean
    +                    count += 1
    +        print(f"  - Sanitized {count} Conversation names.")
    +
    +        db.commit()
    +        print("--- Sanitization Complete. Database committed. ---")
    +
    +    except Exception as e:
    +        print(f"CRITICAL ERROR: {e}")
    +        db.rollback()
    +    finally:
    +        db.close()
    +
    +if __name__ == "__main__":
    +    run_sanitization()
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.