From patchwork Tue Oct 20 20:57:27 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tim Orling X-Patchwork-Id: 101424 Received: from mail-pl1-f181.google.com (mail-pl1-f181.google.com [209.85.214.181]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.2010.1792529859913968616 for ; Tue, 20 Oct 2026 13:57:39 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=LpzcQDQt; spf=pass (domain: gmail.com, ip: 209.85.214.181, mailfrom: ticotimo@gmail.com) Received: by mail-pl1-f181.google.com with SMTP id d9443c01a7336-2dd76b1361dso232725ad.1 for ; Tue, 20 Oct 2026 13:57:39 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1792529859; x=1793134659; darn=lists.yoctoproject.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=F9SE/o8z6hKNH+cHvqLL0T+gEq2TC4h83Gg9ZXdlEL0=; b=LpzcQDQt9BL/pzLzfTKdfeuCQaKjfmW6CtySzGlzrNPrZaqL2JYPVHvqeBuecRjrQv KCzC0J+twQG99aIYi2zLv3LAncAui1Nbh3URQHiBgAMn5ih3qJvlKei8L3K1UDlt3CyJ LWGPLWViFWxhwGxaHAUhvxTla0RnKf93hQ7biZdQzWBMnhoPO71wMkMgUnXg6QaZYOeZ d0GM0r4bgRVUuvrDQgzDrEWxJgwJBT9NmqPZhAH5ejPOlrjUOS/qHfjS+kZonbXSjrEx flFCp+FK3EMQwvZuKmKMWYAk20z6OUIF7Apznh4dpJCIsIvAngVzvlJK3zjYZ01Wt3fB Wjbg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1792529859; x=1793134659; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=F9SE/o8z6hKNH+cHvqLL0T+gEq2TC4h83Gg9ZXdlEL0=; b=qp1sjwZYRfXavND00qlFETJIIA1+mSiKsF7L/fkCWL03FBr8dSHHpGwrduKAF5ElnN konmxI+oaZ+FGDmdSbYpRE71LZAmBcaTMuThwPS4m4p2IsB8XOTDxcS2mF9rnvkXXDbv XhrziG3Ax+7s15ZZlPoCkuUHdDkAHDxL2oKScapVz3j2vGGkKxW//EFweZTTxSySuhYi NmSTc/ICFeikNKqWFKYf4jEjXbSp3ue9FGvQfbODVqJJFl0Hh4J2b1ABcpoYY8PG0y64 Pmwt6Ie+a0bBGh4qFISjrtiZWvFvCXjaRO561+KOY5Kgf0gLWFyAYS9KH+61EVg3YDsv Ybgg== X-Gm-Message-State: AFq9FYKaGCil2AQp12FsfCbteu6/HgCKteZeiB1zcipgUYdRHAhaus87 COKQzPKn4McV6UjfC5Are63MiO+2esQIFTsXQ8f6aXS2g++GSQ6KNKK6ATT1Advd X-Gm-Gg: AYBFou2Gh+2ybE3JUhNOouh07m4p3vYf5YOf3dPaGsuJXh2hVIdrNYl4wbriZhmLSW/ 6Rv+IAOLdeH5Q9slxPmM2vNQRYD4NfO8Ki1QB/p3zW4Yf/0Ozhuo1JRYnxO7IwDp8SFYzM/uUmr fEfEe77cjk7+c8kPdVvF8nSD95cMc7jA1vZRNgX9LDvnfSj//oudgI1sf7zsNx1NpKl46ZFgq4e GVk+a3xv65GYmYrykOkANFyBGjZdWR5y4pF0X7GD+r5Avzn66darptYVimvf7sRHZoG4gUL+3fI ZKx6AEdQ4mMo1c1paCqcatp3UNudU6myw44Ouvdq+sQAJ/BEnVlHcbnu3D3WRc2C00O+7pyz4+R zb40TJ/dgzixsJ49p7jWRdymZJUSewjHKP7kSCfZsoR+mnDEUYROu934BREQ12L3wA0IdZbfWIG /AiBp5cUzRnuufQboy8dy3T/G9fy9baU01kceVJyHH1+fRvv6ildPw+jk39rh8G9ozTiW0sFhqJ 5qSW7g8vduF11XApL9ZwEYSgAT6NGc0Vy19Yfluvw== X-Received: by 2002:a17:90b:5106:b0:3a8:6a04:80ef with SMTP id 98e67ed59e1d1-3ad86dcaf4dmr102711a91.6.1792529859098; Tue, 20 Oct 2026 13:57:39 -0700 (PDT) Return-Path: Received: from localhost.localdomain ([69.9.132.159]) by smtp.gmail.com with ESMTPSA id 98e67ed59e1d1-3ad8029765fsm1039115a91.10.2026.10.20.13.57.36 (version=TLS1_3 cipher=TLS_CHACHA20_POLY1305_SHA256 bits=256/256); Tue, 20 Oct 2026 13:57:36 -0700 (PDT) From: Tim Orling X-Google-Original-From: Tim Orling To: yocto-patches@lists.yoctoproject.org Cc: Tim Orling Subject: [layerindex-web][PATCH 1/4] layerindex/views: fix reflected XSS in recipes CSV export Date: Tue, 20 Oct 2026 13:57:27 -0700 Message-ID: <20261020205730.53370-2-tim.orling@konsulko.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261020205730.53370-1-tim.orling@konsulko.com> References: <20261020205730.53370-1-tim.orling@konsulko.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by lin-tor-korg-lkml-alma10-1 with HTTPS for ; Tue, 20 Oct 2026 20:57:47 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/5114 layer_export_recipes_csv_view echoed an invalid name from the "fields" query parameter back in an HttpResponse with the default text/html content type, so a crafted link could run script on the site without the victim being logged in. Return the error as text/plain with status 400 instead. Reported by CodeQL (py/reflective-xss). AI-Generated: Claude Opus 5.5 Signed-off-by: Tim Orling --- layerindex/views.py | 2 +- tests/test_recipes_csv.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 tests/test_recipes_csv.py diff --git a/layerindex/views.py b/layerindex/views.py index dea0a98..c030ae1 100644 --- a/layerindex/views.py +++ b/layerindex/views.py @@ -1599,7 +1599,7 @@ def layer_export_recipes_csv_view(request, branch, slug): recipe_fields = [f.name for f in Recipe._meta.get_fields() if not (f.auto_created and f.is_relation)] for field in fieldlist: if field not in recipe_fields: - return HttpResponse('Field %s is invalid' % field) + return HttpResponse('Field %s is invalid' % field, content_type='text/plain', status=400) writer = csv.writer(response) for recipe in layerbranch.sorted_recipes(): diff --git a/tests/test_recipes_csv.py b/tests/test_recipes_csv.py new file mode 100644 index 0000000..9ed9084 --- /dev/null +++ b/tests/test_recipes_csv.py @@ -0,0 +1,33 @@ +# layerindex-web - tests for the layer recipes CSV export +# +# Licensed under the MIT license, see COPYING.MIT for details +# +# SPDX-License-Identifier: MIT + +import pytest +from django.test import TestCase +from django.urls import reverse + +from layerindex.models import Branch, LayerItem, LayerBranch + + +@pytest.mark.django_db +class TestRecipesCsv(TestCase): + + def setUp(self): + branch = Branch.objects.create(name='master', bitbake_branch='master') + layer = LayerItem.objects.create(name='meta-test', status='P', layer_type='A', + summary='test', description='test', + vcs_url='git://example.com/meta-test.git') + LayerBranch.objects.create(layer=layer, branch=branch) + self.url = reverse('layer_export_recipes_csv', args=('master', 'meta-test')) + + def test_valid_fields(self): + response = self.client.get(self.url, {'fields': 'pn,pv'}) + self.assertEqual(response.status_code, 200) + self.assertTrue(response['Content-Type'].startswith('text/csv')) + + def test_invalid_field_not_reflected_as_html(self): + response = self.client.get(self.url, {'fields': ''}) + self.assertEqual(response.status_code, 400) + self.assertTrue(response['Content-Type'].startswith('text/plain')) From patchwork Tue Oct 20 20:57:28 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tim Orling X-Patchwork-Id: 101423 Received: from mail-pg1-f169.google.com (mail-pg1-f169.google.com [209.85.215.169]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.2012.1792529862688144959 for ; Tue, 20 Oct 2026 13:57:42 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=S2MUroWk; spf=pass (domain: gmail.com, ip: 209.85.215.169, mailfrom: ticotimo@gmail.com) Received: by mail-pg1-f169.google.com with SMTP id 41be03b00d2f7-cc7e79c1eaaso1555753a12.2 for ; Tue, 20 Oct 2026 13:57:42 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1792529862; x=1793134662; darn=lists.yoctoproject.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=hmlF5MMSRg1UtPpvJ8QuTjientNnF2dIuppmOsR+fA0=; b=S2MUroWkcunmFPVolL0URPTojYWGVICZFeJyL5SAm1s6SuPOs3SeA1A3yU6L0pk8Ox emw/K+FMKfiERV8PeMBBef89T4EjpiSO0LiV48Ye+v35jl9n0HG3VaGIpOnoU8h7p4si t0fsIlGAgNw2aI3VJz86HqMxy9RirndLdgEcp+sSa6VMCq11P/Q03d/+HtfT51T0iTae DOeLn5KD3lrQ7EZo8p8jogXugT2EPkUkU0tK8q5V4u5dE+yda/EODYSPprwv3CbPnZBT oKz3qvhbMXkhiGBXBOo3fXhb9uV7+/crEBXzvxqH6KIVJdqxmnQ8g3MCsj8XLOHu3WaM 4xbw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1792529862; x=1793134662; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=hmlF5MMSRg1UtPpvJ8QuTjientNnF2dIuppmOsR+fA0=; b=KflyDivS1jUHaSIabrP2f9Sq4DdPL8OBtvMlu/ruJS9Ovlq71MnC8u467BOvlwgFQE uKgA0ePq5cfIcgz8cHEjZCMjh7Ax2CDtvuOxuTEiA9d7ebX6vpIH2+9foftRWm3qkYn/ Tgf6GbgO6wdoj0sek5wwgZuVs/Y7+8MMSpLKIejoJEriRZ5pC2nFX2gVen/ayz+ZVVZM +pK4kaNB8vNGpGwEqopyCvjbKxsjdkpBVDqZCPZHfHDLoSeGCGxXuY1RYgqvDSrw8YZy Q6EiTbgH6FmzmsV71ohMiW6MfOEQ/WRaOqY2/oj5vaW+HldLAtUD3x42BhVeHExgg8SB gGWw== X-Gm-Message-State: AFq9FYKogle0lEsJrOyUcswaTF1t8uaVgpj8/pfeiP7Coiv6rwEDq0vX CIbsJ4wwXkycFjm3A0f4cnS5AK49Humbt+wqtFcUcMiTZ2IAkBMUWRQE5Hive3ih X-Gm-Gg: AYBFou3jS/v1Fjp98jQEk0CJgAZ710CrCaes5xpyGV3zZDRXswr3/1plMHSOWYkGfwd 1epQVlB7KNCz3d5WwF6nxmjbS9NyAX3FFkEv2K0dYTOjErcYvs/Y3XJbE0x/bmFRA+On/xv5+Qv sCMm4u44Tc9bzl7JXcS+Wpdc+zqnLDTcG0rZ/00VILgSGqUxlvW15ntcxQkUCJCXyBw/rOeUNiW QBmixOS17dDR6Zrx9WJmwECQV2iwqJPlShepC8zRAeAo3rYnj6zvEGaH9U0NGqIOI7ErmGa7fgS FtBqZzMaStrSIVF+anxif8ARKdyMuzDncTCxuDgB4OGY1tlXYhyByXwsrRqZJD2RWSyrmMgjNXr dPWRzmpoDDWam8VKuVXF9njkzcSVUKLQX31unkK46XGPOAGADk31kyhz3Mj717NbE46oCBsBQef OsF44s1W4ugWeOLK92DIgC+xIMXH6XCfgsjxeOTKPNlbVYnfTaO2FAVD1TXcc2X5WSS8Dkec9Jn RJUJjCYnUv7OQe7ycGXN+lYGFIeyQw= X-Received: by 2002:a17:90b:2b4f:b0:3ab:c80:9365 with SMTP id 98e67ed59e1d1-3ad86d5066dmr97810a91.42.1792529861923; Tue, 20 Oct 2026 13:57:41 -0700 (PDT) Return-Path: Received: from localhost.localdomain ([69.9.132.159]) by smtp.gmail.com with ESMTPSA id 98e67ed59e1d1-3ad8029765fsm1039115a91.10.2026.10.20.13.57.40 (version=TLS1_3 cipher=TLS_CHACHA20_POLY1305_SHA256 bits=256/256); Tue, 20 Oct 2026 13:57:40 -0700 (PDT) From: Tim Orling X-Google-Original-From: Tim Orling To: yocto-patches@lists.yoctoproject.org Cc: Tim Orling Subject: [layerindex-web][PATCH 2/4] layerindex: restrict bulk changes to their owner Date: Tue, 20 Oct 2026 13:57:28 -0700 Message-ID: <20261020205730.53370-3-tim.orling@konsulko.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261020205730.53370-1-tim.orling@konsulko.com> References: <20261020205730.53370-1-tim.orling@konsulko.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by lin-tor-korg-lkml-alma10-1 with HTTPS for ; Tue, 20 Oct 2026 20:57:45 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/5115 The bulk change search and delete views only allow the user who owns the changeset, but the edit, review and patch views did not check ownership or require a login. Anyone could view or edit another user's bulk change, or start patch generation for it. Require a login for all of these views and look the changeset up by owner, so other users' changesets return 404 as the delete view already does. Also require a login for the delete view, which otherwise failed for anonymous users. AI-Generated: Claude Opus 5.5 Signed-off-by: Tim Orling --- layerindex/urls.py | 10 +++---- layerindex/views.py | 23 ++++++++++++-- tests/test_bulkchange_access.py | 53 +++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 tests/test_bulkchange_access.py diff --git a/layerindex/urls.py b/layerindex/urls.py index 51a832d..92f419f 100644 --- a/layerindex/urls.py +++ b/layerindex/urls.py @@ -6,18 +6,18 @@ # # SPDX-License-Identifier: MIT -from django.views.generic import TemplateView, DetailView, ListView, RedirectView +from django.views.generic import TemplateView, ListView, RedirectView from django.views.defaults import page_not_found from django.urls import include, re_path, reverse_lazy from layerindex.views import LayerListView, LayerReviewListView, LayerReviewDetailView, RecipeSearchView, \ MachineSearchView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, \ HistoryListView, EditProfileFormView, AdvancedRecipeSearchView, BulkChangeView, BulkChangeSearchView, \ - bulk_change_edit_view, bulk_change_patch_view, BulkChangeDeleteView, RecipeDetailView, RedirectParamsView, \ + bulk_change_edit_view, bulk_change_patch_view, BulkChangeReviewView, BulkChangeDeleteView, RecipeDetailView, RedirectParamsView, \ ClassicRecipeSearchView, ClassicRecipeDetailView, ClassicRecipeStatsView, LayerUpdateDetailView, UpdateListView, \ UpdateDetailView, StatsView, publish_view, LayerCheckListView, BBClassCheckListView, TaskStatusView, \ ComparisonRecipeSelectView, ComparisonRecipeSelectDetailView, task_log_view, task_stop_view, email_test_view, \ BranchCompareView, RecipeDependenciesView, update_layer_view -from layerindex.models import LayerItem, Recipe, RecipeChangeset +from layerindex.models import LayerItem, Recipe from rest_framework import routers from . import restviews @@ -97,9 +97,7 @@ urlpatterns = [ re_path(r'^bulkchange/(?P\d+)/edit/$', bulk_change_edit_view, {'template_name': 'layerindex/bulkchangeedit.html'}, name="bulk_change_edit"), re_path(r'^bulkchange/(?P\d+)/review/$', - DetailView.as_view( - model=RecipeChangeset, - context_object_name='changeset', + BulkChangeReviewView.as_view( template_name='layerindex/bulkchangereview.html'), name="bulk_change_review"), re_path(r'^bulkchange/(?P\d+)/patches/$', diff --git a/layerindex/views.py b/layerindex/views.py index c030ae1..42722bf 100644 --- a/layerindex/views.py +++ b/layerindex/views.py @@ -307,8 +307,9 @@ def edit_layer_view(request, template_name, branch='master', slug=None): 'return_url': return_url, }) +@login_required def bulk_change_edit_view(request, template_name, pk): - changeset = get_object_or_404(RecipeChangeset, pk=pk) + changeset = get_object_or_404(RecipeChangeset, pk=pk, user=request.user) if request.method == 'POST': formset = BulkChangeEditFormSet(request.POST, queryset=changeset.recipechange_set.all()) @@ -322,8 +323,9 @@ def bulk_change_edit_view(request, template_name, pk): 'formset': formset, }) +@login_required def bulk_change_patch_view(request, pk): - changeset = get_object_or_404(RecipeChangeset, pk=pk) + changeset = get_object_or_404(RecipeChangeset, pk=pk, user=request.user) # FIXME this couples the web server and machine running the update script together, # but given that it's a separate script the way is open to decouple them in future try: @@ -855,10 +857,27 @@ class BaseDeleteView(DeleteView): return context +class BulkChangeReviewView(DetailView): + model = RecipeChangeset + context_object_name = 'changeset' + + @method_decorator(login_required) + def dispatch(self, request, *args, **kwargs): + return super(BulkChangeReviewView, self).dispatch(request, *args, **kwargs) + + def get_queryset(self): + qs = super(BulkChangeReviewView, self).get_queryset() + return qs.filter(user=self.request.user) + + class BulkChangeDeleteView(BaseDeleteView): model = RecipeChangeset success_url = reverse_lazy('bulk_change') + @method_decorator(login_required) + def dispatch(self, request, *args, **kwargs): + return super(BulkChangeDeleteView, self).dispatch(request, *args, **kwargs) + def get_queryset(self): qs = super(BulkChangeDeleteView, self).get_queryset() return qs.filter(user=self.request.user) diff --git a/tests/test_bulkchange_access.py b/tests/test_bulkchange_access.py new file mode 100644 index 0000000..e85451d --- /dev/null +++ b/tests/test_bulkchange_access.py @@ -0,0 +1,53 @@ +# layerindex-web - tests for bulk change access control +# +# Licensed under the MIT license, see COPYING.MIT for details +# +# SPDX-License-Identifier: MIT + +import pytest +from django.contrib.auth.models import User +from django.test import TestCase +from django.urls import reverse + +from layerindex.models import RecipeChangeset + + +@pytest.mark.django_db +class TestBulkChangeAccess(TestCase): + + def setUp(self): + self.owner = User.objects.create_user('owner', 'owner@example.com', 'pw') + self.other = User.objects.create_user('other', 'other@example.com', 'pw') + self.changeset = RecipeChangeset.objects.create(user=self.owner, name='mine') + + def _url(self, name): + return reverse(name, args=(self.changeset.pk,)) + + def test_anonymous_redirected_to_login(self): + for name in ('bulk_change_edit', 'bulk_change_review', 'bulk_change_patches', 'bulk_change_delete'): + response = self.client.get(self._url(name)) + self.assertEqual(response.status_code, 302, name) + self.assertIn('login', response['Location'], name) + + def test_other_user_gets_404(self): + self.client.force_login(self.other) + for name in ('bulk_change_edit', 'bulk_change_review', 'bulk_change_patches', 'bulk_change_delete'): + response = self.client.get(self._url(name)) + self.assertEqual(response.status_code, 404, name) + response = self.client.post(self._url('bulk_change_delete')) + self.assertEqual(response.status_code, 404) + self.assertTrue(RecipeChangeset.objects.filter(pk=self.changeset.pk).exists()) + + def test_other_user_cannot_post_edits(self): + self.client.force_login(self.other) + # A valid (empty) edit formset, which would be saved and redirect + # to the review page if the changeset were found + data = {'form-TOTAL_FORMS': '0', 'form-INITIAL_FORMS': '0'} + response = self.client.post(self._url('bulk_change_edit'), data) + self.assertEqual(response.status_code, 404) + + def test_owner_can_access(self): + self.client.force_login(self.owner) + for name in ('bulk_change_edit', 'bulk_change_review'): + response = self.client.get(self._url(name)) + self.assertEqual(response.status_code, 200, name) From patchwork Tue Oct 20 20:57:29 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tim Orling X-Patchwork-Id: 101425 Received: from mail-pl1-f175.google.com (mail-pl1-f175.google.com [209.85.214.175]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.2013.1792529865606792319 for ; Tue, 20 Oct 2026 13:57:45 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=Sx5ty3gv; spf=pass (domain: gmail.com, ip: 209.85.214.175, mailfrom: ticotimo@gmail.com) Received: by mail-pl1-f175.google.com with SMTP id d9443c01a7336-2e831e86ebfso40075ad.1 for ; Tue, 20 Oct 2026 13:57:45 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1792529865; x=1793134665; darn=lists.yoctoproject.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=WPcE02lrZfWwY+OCoO1LnUwR3P3XVsUx+DNDWxOCV3s=; b=Sx5ty3gvGi5mDBoyphinG7HrTRSG8tB2lboI35VvAaqtZJTOCEFyQumjdMVnnLaBWY EzC3bt4EddkjKKSAPTBkCepfnlxfx7tap2YuA/1O1WGMRGUUPcwV16RpkzaHgbwVLvGf h28vTdq0IgBrm97MZexZdwzKcmyqFxhhH07DL7C0SxdoLlyPaX+UWsbQ2qWHamYPmLNW bihoILqNED6UJ0SsZEm3Osu2pfHK9D3e2L87QjKN2c1Lc+qQGp7U/YoIhIujhfDtADB2 KsW1uPgnRC6IzbLfsDaTmSQKYD287LdYhuD8zQyJ+s3nSQoX47C3Aqa9/JB1BZSFGS/W f8Vw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1792529865; x=1793134665; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=WPcE02lrZfWwY+OCoO1LnUwR3P3XVsUx+DNDWxOCV3s=; b=AP3cIBaoeqyiRJ8iWlFAZ80N4wD3e+YkmufhYJiRuky3/uObS/RIp6h8JZ2/qiZcS9 uhTZZVZsEdfzG0gQnPIAJa90CMkI62A8Z91AP74tEkwTNCrTTOH0hvSl72qXIbEAOGR0 fVdYsM09FhUoXeFXj6WAH6fldhgdI3y9Jjb7MAA9zA/h212seoFpHliDNmq/ONQnqWjL JbxbspSUam0aZMPSstd26ByIvfa5X1eKNITRrdhkjOrEehk15LpvpyIWa6KmBOhcsa/x wlggXpbq+8p/xFaYBU4hJi4Hzx40w7Dtnzl5NpHDqfSpXrbXZIOvO5Q468ImoW92cjM4 Firg== X-Gm-Message-State: AFq9FYI+0KNOb6xKmv/MGhi+NiP44K98tQCJfTR7TFlkeo4fYbpKNWhq Vmf/Tqt+32QA98AmxtESQaD95x0GmRi2JNdx+sL982mg2+dMIVbk0aaf8QvxF9Sc X-Gm-Gg: AYBFou2fWCqrWjflmBjTragNIsJMQBldPAuhOiaGn9Bq28W47jFoCd7RZJYxCxm+lFS ysrewttDpDV4mmMG79pdiskaVUrWD5dydBNxlPb7i6XnFeIDCTQELXdNxoUE7RBgf6yiFyn2ilH +eoy2PAottg0mybcvxQXFYzwxrR7DU5ibV9cF6ufD0m3F6i9sZp+e1bQ4P+drlWA2PrWJVCyPxx Q4JKcWVRhl4jxV8k6fxoDwOLwC/6jBZi4NJYOqOe0DjRaOkOiEj7adWvZcbEbmgxvoZd1WIGQnr +WOgT1LF8IeedGZN+uUBFkfAQ9uIj4cQcGEhutUFb4BQ9Ofz20kW71SlApAZxP7jgQjRCpbxyv7 Y842dmG5NWnhhrokbTPj7awjYWUykrKPVMB7bajOkprZGkG4FoKRysnkLIi+m0ae6GRbhJS0ZHR Z4wLn/3g41W3ED0BvLOBcg1CJ+ZUBCv4NQd9ASayDiGdUEdE5JExm46bnYijbldT/TshqETRPAF yQoRpehr8g7BRe8RFeY8yAPZSzkYJlu X-Received: by 2002:a17:90b:6cc:b0:3a8:5fd6:cb6 with SMTP id 98e67ed59e1d1-3ad86a402ebmr169502a91.0.1792529864848; Tue, 20 Oct 2026 13:57:44 -0700 (PDT) Return-Path: Received: from localhost.localdomain ([69.9.132.159]) by smtp.gmail.com with ESMTPSA id 98e67ed59e1d1-3ad8029765fsm1039115a91.10.2026.10.20.13.57.43 (version=TLS1_3 cipher=TLS_CHACHA20_POLY1305_SHA256 bits=256/256); Tue, 20 Oct 2026 13:57:43 -0700 (PDT) From: Tim Orling X-Google-Original-From: Tim Orling To: yocto-patches@lists.yoctoproject.org Cc: Tim Orling Subject: [layerindex-web][PATCH 3/4] layerindex/views: don't expose patch generation errors to users Date: Tue, 20 Oct 2026 13:57:29 -0700 Message-ID: <20261020205730.53370-4-tim.orling@konsulko.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261020205730.53370-1-tim.orling@konsulko.com> References: <20261020205730.53370-1-tim.orling@konsulko.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by lin-tor-korg-lkml-alma10-1 with HTTPS for ; Tue, 20 Oct 2026 20:57:47 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/5116 bulk_change_patch_view returned the output of the failing bulkchange.py run, or the exception text, in the HTTP response. That can include tracebacks, server paths and bitbake output. Log the details instead and return a generic error with status 500. The lock timeout message is kept, since it tells the user what to do and reveals nothing. Reported by CodeQL (py/stack-trace-exposure). AI-Generated: Claude Opus 5.5 Signed-off-by: Tim Orling --- layerindex/views.py | 14 ++++++++------ tests/test_bulkchange_access.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/layerindex/views.py b/layerindex/views.py index 42722bf..592311f 100644 --- a/layerindex/views.py +++ b/layerindex/views.py @@ -6,6 +6,7 @@ # # SPDX-License-Identifier: MIT +import logging import os import sys import re @@ -63,6 +64,8 @@ from layerindex.models import (BBAppend, BBClass, BBClassGlobal, BBClassRecipe, from . import tasks, utils +logger = logging.getLogger(__name__) + def edit_layernote_view(request, template_name, slug, pk=None): layeritem = get_object_or_404(LayerItem, name=slug) if layeritem.comparison: @@ -347,12 +350,11 @@ def bulk_change_patch_view(request, pk): return HttpResponse('No patch data generated', content_type='text/plain') except Exception as e: output = getattr(e, 'output', None) - if output: - if 'timeout' in output: - return HttpResponse('Failed to generate patches: timed out waiting for lock. Please try again shortly.', content_type='text/plain') - else: - return HttpResponse('Failed to generate patches: %s' % output, content_type='text/plain') - return HttpResponse('Failed to generate patches: %s' % e, content_type='text/plain') + if output and 'timeout' in output: + return HttpResponse('Failed to generate patches: timed out waiting for lock. Please try again shortly.', content_type='text/plain') + # Details can include paths and bitbake output, so keep them in the server log + logger.exception('Failed to generate patches for bulk change %s: %s', pk, output) + return HttpResponse('Failed to generate patches, please contact the site administrator.', content_type='text/plain', status=500) # FIXME better error handling diff --git a/tests/test_bulkchange_access.py b/tests/test_bulkchange_access.py index e85451d..914493c 100644 --- a/tests/test_bulkchange_access.py +++ b/tests/test_bulkchange_access.py @@ -51,3 +51,14 @@ class TestBulkChangeAccess(TestCase): for name in ('bulk_change_edit', 'bulk_change_review'): response = self.client.get(self._url(name)) self.assertEqual(response.status_code, 200, name) + + def test_patch_error_details_not_exposed(self): + import subprocess + from unittest import mock + self.client.force_login(self.owner) + err = subprocess.CalledProcessError(1, 'bulkchange.py', output='Traceback: /opt/secret/path') + with mock.patch('layerindex.views.utils.runcmd', side_effect=err), \ + self.assertLogs('layerindex.views', 'ERROR'): + response = self.client.get(self._url('bulk_change_patches')) + self.assertEqual(response.status_code, 500) + self.assertNotIn(b'/opt/secret/path', response.content) From patchwork Tue Oct 20 20:57:30 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tim Orling X-Patchwork-Id: 101426 Received: from mail-pl1-f170.google.com (mail-pl1-f170.google.com [209.85.214.170]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.2045.1792529868875142558 for ; Tue, 20 Oct 2026 13:57:48 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=bKgw+e5C; spf=pass (domain: gmail.com, ip: 209.85.214.170, mailfrom: ticotimo@gmail.com) Received: by mail-pl1-f170.google.com with SMTP id d9443c01a7336-2e977109392so254065ad.0 for ; Tue, 20 Oct 2026 13:57:48 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1792529868; x=1793134668; darn=lists.yoctoproject.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=P6+ltMe/m1TVuW6FT7wcTh3/jF8QyOA0hPVWyXIcBlE=; b=bKgw+e5Cg8GPFdQ6aw2FC+m2Cr8vpV0sN6sept7B6fAWhFzCnxX5x1aMxlDHBRvF7o MMOGORTw/SacNhw4jKqYNNhSnumISwB0lSumw5iD/mk7X1O+DHlLmDLUpY2t5PA/4aed sujXShEu3ROaUIQcBGM2xxA0VRMVuha6s998wrr8NoBw6bZHwp7nMT+9lci/iD8bics1 jYIfH3VgOGDv7mqYkkXIjCpTwx7lOnt+RRB29O+D8l0bKf9NFd1qmTR7g7j88c6WaFZo whpr62oKsdnORbcjcDzWk82eWoyjKBX/nFTRTi+wWbzCBLpVk2qg9Ot2bOQZKE338Su4 Zorw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1792529868; x=1793134668; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=P6+ltMe/m1TVuW6FT7wcTh3/jF8QyOA0hPVWyXIcBlE=; b=D4PbwMEYGAABuj51yugs22Y8MT0/VSGB7rxumlT0M+JzmxEXGxuZx+TqYjTvmw5sDq w5wGYtFWyPBE5EoG7RTmFARU5dvjSwOegOb36tFVjuHPXAc1h5c/FV9TseX7UYDuLfPA g8sCStPlrQm1kS9vvOGSQcii0lrKJo8jmFmRanplRoGcoJb6hOK+aCe93fBtdFio1JJn hl8pVd4B6AqvdrSNJ6+e9wIH9eYYx8SfqmK7uwgpqZdXhCJR8XG2cv5MBVWTDFtKiyiT Fm+1lggSbCZhtLQw/z+yamD7Ng+moUL+Kox+KezKGy/ejIwJr5vhkXqLLAuDm7YkFUT8 64vQ== X-Gm-Message-State: AFq9FYJ54i3TXUjhV3pENmSFVn7+O0XBIjoDhPKJY7Hy6zHgyTRXJQW+ /N47naMAvg+Ub9EW1rN8jStzcLAm5s/2t5pbnprqPViPJ8KbuKZ8L1F/6D4dL/zu X-Gm-Gg: AYBFou0+WyhNEeuijUYMZ3NLv/Aeqzue/mFSX7Hb63gff3zG0pvlIPblluAzVjY0PQM D0IUZplSj0ExJAKLgLV9qR5wdoh6EjQLY5ekgkdA69dbWYlrQjJZF/cMPRXmUuTeshBnEYk54u4 NlV0CBOEwvB5RCW0tCbMYdADQPhFcRlS7ASAcbBgRqQhMvvBfC+jKqobcDRKZkSeG3pAo/2p0Zp //bAoQ4A6JB3q+LuR5pFmW2MkLJwG02bvSGIkJtBJTQH4qOHlW8VnxPSJlJj6JlwuD3ii3TA2qr x4JC2sby4RqOn91DuQu3KMLtkU/U0SiMjnax2KdDIWyI1Eq3XsCRPhNn1y7mziAKZXNgxoWPh0w 1wTQp1ukgORC2R9g7bySiQsUm2KTMaYZ4ks1TASk3C/08uh/fhQaGUDRXFbasgJaG1uMSmgakDg JHV81qW6V4qm8yHf3arombyr+2dCupnyXCuXXvGU01OFE+v93IsJJ1B2Jdz3kI+0u4l9her4Ck8 5SX2Kabdvfxa+WhhbgHP+nA+IqBHcuJ2i5RkWVX821+oX/t/fa4epK/CgnODFoKajXau0bSv7X7 DACPKE7FzC6v X-Received: by 2002:a17:90b:4d09:b0:3ab:2c2b:1030 with SMTP id 98e67ed59e1d1-3ad86e9e4f4mr90264a91.22.1792529868192; Tue, 20 Oct 2026 13:57:48 -0700 (PDT) Return-Path: Received: from localhost.localdomain ([69.9.132.159]) by smtp.gmail.com with ESMTPSA id 98e67ed59e1d1-3ad8029765fsm1039115a91.10.2026.10.20.13.57.45 (version=TLS1_3 cipher=TLS_CHACHA20_POLY1305_SHA256 bits=256/256); Tue, 20 Oct 2026 13:57:46 -0700 (PDT) From: Tim Orling X-Google-Original-From: Tim Orling To: yocto-patches@lists.yoctoproject.org Cc: Tim Orling Subject: [layerindex-web][PATCH 4/4] tests: check that changing the profile email requires reactivation Date: Tue, 20 Oct 2026 13:57:30 -0700 Message-ID: <20261020205730.53370-5-tim.orling@konsulko.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261020205730.53370-1-tim.orling@konsulko.com> References: <20261020205730.53370-1-tim.orling@konsulko.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by lin-tor-korg-lkml-alma10-1 with HTTPS for ; Tue, 20 Oct 2026 20:57:57 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/yocto-patches/message/5117 Layer edit rights are granted by matching a user's email address against the layer's maintainer emails, which are public. Changing the email on the profile page therefore must not take effect until the new address is confirmed: the user is logged out and deactivated, and the activation link is sent only to the new address. Add a test so this protection cannot be removed without notice. AI-Generated: Claude Opus 5.5 Signed-off-by: Tim Orling --- tests/test_profile_email.py | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/test_profile_email.py diff --git a/tests/test_profile_email.py b/tests/test_profile_email.py new file mode 100644 index 0000000..79e4adf --- /dev/null +++ b/tests/test_profile_email.py @@ -0,0 +1,67 @@ +# layerindex-web - tests for changing the email address on the profile page +# +# Licensed under the MIT license, see COPYING.MIT for details +# +# SPDX-License-Identifier: MIT + +import pytest +from captcha.models import CaptchaStore +from django.contrib.auth import authenticate +from django.contrib.auth.models import User +from django.core import mail +from django.test import RequestFactory, TestCase +from django.urls import reverse + +from layerindex.models import SecurityQuestion, SecurityQuestionAnswer, UserProfile + + +@pytest.mark.django_db +class TestProfileEmailChange(TestCase): + """Changing the email address must not take effect without proving + ownership of the new address, since layer edit rights are granted by + matching the user's email against (public) maintainer emails.""" + + def setUp(self): + self.user = User.objects.create_user('someone', 'old@example.com', 'pw') + profile = UserProfile.objects.create(user=self.user) + self.questions = [SecurityQuestion.objects.create(question='Question %d' % i) for i in range(3)] + for question in self.questions: + SecurityQuestionAnswer.objects.create(user=profile, security_question=question, answer='x') + self.client.force_login(self.user) + + def _post_profile(self, email): + hashkey = CaptchaStore.generate_key() + data = { + 'username': 'someone', + 'first_name': '', + 'last_name': '', + 'email': email, + 'captcha_0': hashkey, + 'captcha_1': CaptchaStore.objects.get(hashkey=hashkey).response, + 'answer_1': '*****', + 'answer_2': '*****', + 'answer_3': '*****', + } + for i, question in enumerate(self.questions, 1): + data['security_question_%d' % i] = question.pk + return self.client.post(reverse('profile'), data) + + def test_email_change_requires_reactivation(self): + response = self._post_profile('maintainer@example.com') + self.assertRedirects(response, reverse('reregister'), fetch_redirect_response=False) + + self.user.refresh_from_db() + self.assertFalse(self.user.is_active) + # Logged out, and an inactive account cannot log back in + self.assertNotIn('_auth_user_id', self.client.session) + self.assertIsNone(authenticate(RequestFactory().post('/'), username='someone', password='pw')) + # The activation link only goes to the new address + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].to, ['maintainer@example.com']) + + def test_unchanged_email_keeps_account_active(self): + response = self._post_profile('old@example.com') + self.assertEqual(response.status_code, 302) + self.user.refresh_from_db() + self.assertTrue(self.user.is_active) + self.assertEqual(len(mail.outbox), 0)