nene2-python 1.8.38__tar.gz → 1.8.40__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nene2_python-1.8.38 → nene2_python-1.8.40}/PKG-INFO +1 -1
- nene2_python-1.8.40/docs/field-trials/2026-05-field-trial-168.md +473 -0
- nene2_python-1.8.40/docs/field-trials/2026-05-field-trial-169.md +272 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/pyproject.toml +1 -1
- {nene2_python-1.8.38 → nene2_python-1.8.40}/uv.lock +1 -1
- {nene2_python-1.8.38 → nene2_python-1.8.40}/.env.example +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/.gitignore +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/AGENTS.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/CHANGELOG.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/CLAUDE.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/Dockerfile +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/LICENSE +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/README.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/alembic/README +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/alembic/env.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/alembic.ini +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/compose.yaml +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/de/index.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-100.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-101.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-102.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-103.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-104.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-105.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-106.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-107.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-108.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-109.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-110.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-111.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-112.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-113.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-114.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-115.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-116.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-117.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-118.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-119.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-120.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-121.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-122.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-123.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-124.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-125.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-126.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-127.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-128.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-129.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-130.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-131.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-132.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-133.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-134.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-135.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-136.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-137.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-138.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-139.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-140.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-141.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-142.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-143.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-144.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-145.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-146.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-147.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-148.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-149.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-150.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-151.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-152.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-153.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-154.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-155.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-156.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-157.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-158.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-159.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-160.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-161.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-162.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-163.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-164.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-165.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-166.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-167.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-97.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-98.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/field-trials/2026-05-field-trial-99.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/fr/index.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/api-versioning.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/background-tasks.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/cors.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/custom-auth-middleware.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/dependency-injection.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/domain-events.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/file-upload.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/lifespan-and-app-state.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/response-patterns.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/soft-delete.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/streaming.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/structured-logging.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/how-to/webhook.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/index.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/ja/index.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/reference/api.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/roadmap.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/templates/field-trial-report.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/todo/current.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/zh/index.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/package-lock.json +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/package.json +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/__main__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/app.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/mcp.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/schema.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/cache/ttl.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/security/webhook.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/conftest.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/cache/test_ttl.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/security/test_webhook.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.38 → nene2_python-1.8.40}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.40
|
|
4
4
|
Summary: NENE2 Python — minimal API framework following NENE2's design philosophy
|
|
5
5
|
Project-URL: Homepage, https://github.com/hideyukiMORI/nene2-python
|
|
6
6
|
Project-URL: Repository, https://github.com/hideyukiMORI/nene2-python
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
# FT168: re モジュール
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-21
|
|
4
|
+
**テーマ**: `re` モジュール — 正規表現の基本・名前付きグループ・入力バリデーション・ReDoS 対策
|
|
5
|
+
**セキュリティ診断**: **あり**(168 % 3 = 0)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
Python 標準ライブラリの `re` モジュールを nene2-python フレームワーク上で検証した。
|
|
12
|
+
`re` は HTTP 入力バリデーション・ログ解析・テキスト変換で広く使われるが、
|
|
13
|
+
ReDoS(正規表現サービス妨害)は Django・CPython 本体でも過去に CVE が報告された
|
|
14
|
+
攻撃ベクターであり、FT168 のセキュリティ診断の中心テーマとした。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 実装したサンプルアプリ
|
|
19
|
+
|
|
20
|
+
**場所**: `/home/xi/docker/nene2-python-FT/ft168-re/`
|
|
21
|
+
|
|
22
|
+
### 主要機能
|
|
23
|
+
|
|
24
|
+
| 関数/定数 | 概要 |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `find_all_digits()` / `find_all_words()` | コンパイル済みパターンで全マッチを抽出 |
|
|
27
|
+
| `match_start()` / `search_anywhere()` / `fullmatch_entire()` | 3 つのマッチモードを比較 |
|
|
28
|
+
| `parse_date()` / `parse_log_line()` | `(?P<name>...)` 名前付きグループで構造化データ抽出 |
|
|
29
|
+
| `sanitize_html_tags()` / `normalize_whitespace()` / `censor_credit_card()` | テキスト変換ユーティリティ |
|
|
30
|
+
| `sub_count()` | `re.subn()` で置換数を返す |
|
|
31
|
+
| `EMAIL_RE` / `IPV4_RE` / `JP_PHONE_RE` / `SLUG_RE` | 量化子を明示的に制限した安全なバリデーションパターン |
|
|
32
|
+
| `benchmark_redos_safe()` | 脆弱パターン vs 安全パターンの実行時間比較(max 100文字ガード付き) |
|
|
33
|
+
| `extract_urls()` / `extract_sections()` | `re.finditer()` による位置情報付き抽出 |
|
|
34
|
+
| `split_on_delimiters()` / `highlight_keyword()` | `re.escape()` によるインジェクション防止パターン |
|
|
35
|
+
|
|
36
|
+
### HTTP エンドポイント
|
|
37
|
+
|
|
38
|
+
| メソッド | パス | 概要 |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| GET | `/re/search` | 数字・単語の全マッチ抽出 |
|
|
41
|
+
| GET | `/re/match-modes` | match / search / fullmatch の比較 |
|
|
42
|
+
| GET | `/re/parse-date` | 名前付きグループによる日付解析 |
|
|
43
|
+
| GET | `/re/parse-log` | ログ行の構造化解析 |
|
|
44
|
+
| GET | `/re/sanitize` | HTML タグ除去・空白正規化・クレジットカードマスク |
|
|
45
|
+
| POST | `/re/sub` | ユーザー指定パターンによる置換(re.error を捕捉) |
|
|
46
|
+
| GET | `/re/validate` | メール・IPv4・電話・スラッグのバリデーション |
|
|
47
|
+
| POST | `/re/redos-benchmark` | ReDoS 安全/脆弱パターンの実行時間比較(100文字上限) |
|
|
48
|
+
| GET | `/re/extract-urls` | URL 抽出(位置情報付き) |
|
|
49
|
+
| GET | `/re/extract-sections` | Markdown 見出し抽出 |
|
|
50
|
+
| POST | `/re/split` | 複数区切り文字で分割 |
|
|
51
|
+
| POST | `/re/highlight` | キーワードハイライト(re.escape で安全化) |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## テスト結果
|
|
56
|
+
|
|
57
|
+
**46 passed(摩擦1件・修正済み)**
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
46 passed in 0.41s
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 摩擦ポイント
|
|
66
|
+
|
|
67
|
+
### F-1: `highlight_keyword()` のケースインセンシティブ置換でテスト期待値が誤り(深刻度: 低)
|
|
68
|
+
|
|
69
|
+
**事象**: `highlight_keyword("Hello World", "world")` は `**world**`(小文字)を返すが、
|
|
70
|
+
テストが `**World**`(大文字)を期待していた。
|
|
71
|
+
**原因**: `re.sub()` の置換文字列 `f"**{keyword}**"` は入力した `keyword` をそのまま使う。
|
|
72
|
+
マッチした文字ではなく、呼び出し時のキーワード文字列が埋め込まれる。
|
|
73
|
+
**対応**: テストの期待値を `**world**` に修正。関数の動作は正しい。
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 観察点
|
|
78
|
+
|
|
79
|
+
### 観察1: `re.compile()` をモジュールレベルで行い再コンパイルを避ける
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
EMAIL_RE: re.Pattern[str] = re.compile(
|
|
83
|
+
r"^[a-zA-Z0-9._%+\-]{1,64}@[a-zA-Z0-9.\-]{1,253}\.[a-zA-Z]{2,63}$"
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`re.compile()` はキャッシュを持つが、モジュールレベルで定数として定義すると
|
|
88
|
+
型注釈 (`re.Pattern[str]`) がつき、意図が明確になる。
|
|
89
|
+
頻繁に呼ばれるバリデーションは必ずモジュールレベルでコンパイルする。
|
|
90
|
+
|
|
91
|
+
### 観察2: 名前付きグループ `(?P<name>...)` + `groupdict()` で構造化データを返す
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
DATE_PATTERN = re.compile(
|
|
95
|
+
r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def parse_date(text: str) -> dict[str, str] | None:
|
|
99
|
+
m = DATE_PATTERN.search(text)
|
|
100
|
+
if m is None:
|
|
101
|
+
return None
|
|
102
|
+
return m.groupdict()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`groupdict()` で名前付きグループを辞書として取得できる。
|
|
106
|
+
インデックス番号ベースの `group(1)` よりも可読性が高く、パターン変更時にも安全。
|
|
107
|
+
|
|
108
|
+
### 観察3: 量化子に明示的な上限を設けてバリデーションパターンを ReDoS から守る
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
# 危険(量化子無制限)
|
|
112
|
+
bad_email = re.compile(r"^[\w.]+@[\w.]+\.[a-z]+$")
|
|
113
|
+
|
|
114
|
+
# 安全(量化子に上限)
|
|
115
|
+
safe_email = re.compile(
|
|
116
|
+
r"^[a-zA-Z0-9._%+\-]{1,64}@[a-zA-Z0-9.\-]{1,253}\.[a-zA-Z]{2,63}$"
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
無制限の `+` や `*` の組み合わせが「nested quantifier」を形成すると
|
|
121
|
+
バックトラッキングが指数的に増加する。
|
|
122
|
+
メールアドレスのローカル部は最大 64 文字(RFC 5321)など、仕様がわかっている場合は
|
|
123
|
+
必ず `{1,N}` の形式で上限を書く。
|
|
124
|
+
|
|
125
|
+
### 観察4: `re.escape()` でユーザー入力を安全にパターンに組み込む
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
def highlight_keyword(text: str, keyword: str) -> str:
|
|
129
|
+
safe_keyword = re.escape(keyword)
|
|
130
|
+
return re.sub(safe_keyword, f"**{keyword}**", text, flags=re.IGNORECASE)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`keyword` に `.` `*` `[` 等の正規表現メタ文字が含まれていても
|
|
134
|
+
`re.escape()` がエスケープするため、インジェクションにならない。
|
|
135
|
+
ユーザー入力をパターンの一部として使う場合は必ず `re.escape()` を通す。
|
|
136
|
+
|
|
137
|
+
### 観察5: `re.error` を明示的にハンドリングしてユーザー定義パターンを安全に受け付ける
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
try:
|
|
141
|
+
result, count = sub_count(body.pattern, body.replacement, body.text)
|
|
142
|
+
except re.error as exc:
|
|
143
|
+
return JSONResponse({"error": f"Invalid regex: {exc}"}, status_code=422)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
ユーザーがパターンを指定できるエンドポイントでは `re.error` が必ず必要。
|
|
147
|
+
FastAPI の Pydantic バリデーションは `re.error` を捕捉しない。
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## nene2-python フレームワークとの統合
|
|
152
|
+
|
|
153
|
+
- `EMAIL_RE` / `SLUG_RE` のような量化子制限済みパターンは nene2 の `validation` モジュールに統合できる
|
|
154
|
+
- `RequestIdMiddleware` の `_UUID_V4_RE` は既に安全なパターン設計(固定長・固定フォーマット)になっており本 FT の手法と一致している
|
|
155
|
+
- ユーザー入力をパターンとして受け付ける API は nene2 標準では提供しない(使う場合は必ず `max_length` + `re.error` ハンドリング必須)
|
|
156
|
+
- HTTP 境界の文字列バリデーションで `re.fullmatch()` + コンパイル済みパターンを使うパターンは how-to に追加価値あり
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Developer Experience (DX) Review
|
|
161
|
+
|
|
162
|
+
### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
|
|
163
|
+
|
|
164
|
+
`re.match()` と `re.search()` の違いを最初に混乱する。
|
|
165
|
+
「match は先頭から、search はどこでも」という概念は一度聞けばわかるが、
|
|
166
|
+
公式ドキュメントの説明だけでは実体験が必要。
|
|
167
|
+
|
|
168
|
+
**ドキュメント理解**: `re.compile()` してから使う理由が分かりにくい。
|
|
169
|
+
「毎回 re.match() を呼ぶより速い」という説明では動機が弱い。
|
|
170
|
+
「型注釈がつくので mypy が通る」という nene2 的な理由の方が刺さる。
|
|
171
|
+
|
|
172
|
+
**事故リスク**: 中。`re.match(r"\d+", "abc123")` が `None` を返しても `None.group()` するまでエラーにならない。
|
|
173
|
+
`if m := re.match(...):` のウォルラス演算子パターンを最初に覚えさせると安全。
|
|
174
|
+
|
|
175
|
+
**規約の使いやすさ**: `EMAIL_RE = re.compile(...)` というモジュールレベル定数パターンはコピペできる。
|
|
176
|
+
難しいのは「正しいパターンを書く」こと。バリデーション目的のパターンは nene2 が提供するべき。
|
|
177
|
+
|
|
178
|
+
### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
|
|
179
|
+
|
|
180
|
+
`re.sub()` で HTML タグを除去するパターンは既に知っている。
|
|
181
|
+
`re.escape()` の必要性は知らず、ユーザー入力を直接 `re.sub(user_input, ...)` に渡す可能性がある。
|
|
182
|
+
|
|
183
|
+
**コピペ可能性**: 高。`re.compile()` + `fullmatch()` のパターンは一目で理解できる。
|
|
184
|
+
|
|
185
|
+
**拡張時の罠**: `re.match()` で検証しているつもりが、パターンに `$` がないため末尾に余分な文字が許容される。
|
|
186
|
+
例: `re.match(r"\d+", "123abc")` は `True` を返す(`$` がないため先頭の数字だけマッチ)。
|
|
187
|
+
**`fullmatch_entire()` を使う規約** が事故を防ぐ。
|
|
188
|
+
|
|
189
|
+
**セキュリティ的な事故リスク**: 高。`re.escape()` を知らずにユーザー入力をパターンに組み込むと
|
|
190
|
+
正規表現インジェクションになりうる(悪意あるパターンによる任意の文字列マッチング)。
|
|
191
|
+
|
|
192
|
+
### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
|
|
193
|
+
|
|
194
|
+
TypeScript の `RegExp` と Python の `re` は概念的に近い。
|
|
195
|
+
`/pattern/g` フラグが Python では `re.compile(pattern, re.IGNORECASE)` になる点は慣れが必要。
|
|
196
|
+
名前付きキャプチャグループの構文が `(?P<name>...)` と冗長に見える。
|
|
197
|
+
|
|
198
|
+
**エラーレスポンスの質**: `/re/sub` エンドポイントは `re.error` を捕捉して 422 を返す。
|
|
199
|
+
クライアントには `{"error": "Invalid regex: ..."}` が届き、デバッグしやすい。
|
|
200
|
+
|
|
201
|
+
**事故リスク**: 低。概念の対応関係が作れる。
|
|
202
|
+
フロントエンドでもサーバーサイドでも同じ正規表現を使う場合、Python 側に `re.escape()` が必要という点は盲点になりうる。
|
|
203
|
+
|
|
204
|
+
### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
|
|
205
|
+
|
|
206
|
+
Django の `RegexValidator` と nene2 の手動 `fullmatch()` を比較する。
|
|
207
|
+
Django は validators に委ねるが、nene2 は関数として明示的に書く。
|
|
208
|
+
|
|
209
|
+
**他フレームワークとの差異**:
|
|
210
|
+
- Django の `EmailValidator` は過去に ReDoS の CVE (CVE-2019-19844) が出ている。
|
|
211
|
+
nene2 は自前でパターンを書くため、設計者の責任範囲が広い。
|
|
212
|
+
- FastAPI + Pydantic v2 の `EmailStr` / `AnyHttpUrl` は validators ライブラリに委ねており、
|
|
213
|
+
本番品質のバリデーションを使いたい場合はそちらが安全。
|
|
214
|
+
|
|
215
|
+
**本番投入可能性**: 条件付き。自前バリデーションパターンは必ず ReDoS レビューを通過させること。
|
|
216
|
+
`validators` / `pydantic[email]` を使う方が安全な選択肢。
|
|
217
|
+
|
|
218
|
+
### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
|
|
219
|
+
|
|
220
|
+
**コードレビューチェックポイント**:
|
|
221
|
+
- [ ] バリデーションパターンに `{1,N}` の上限があるか(`+` や `*` の無制限使用を禁止)
|
|
222
|
+
- [ ] ユーザー入力をパターンに組み込む場合に `re.escape()` を通しているか
|
|
223
|
+
- [ ] `re.match()` でなく `re.fullmatch()` でバリデーションしているか(末尾のゴミを許容しないか)
|
|
224
|
+
- [ ] `re.error` を呼び出し元でハンドリングしているか(特に外部パターン受付エンドポイント)
|
|
225
|
+
- [ ] `re.compile()` はモジュールレベルで行い、リクエストごとの再コンパイルを避けているか
|
|
226
|
+
|
|
227
|
+
**チームでの安全な共有パターン**: バリデーション用パターンは `src/nene2/validation/patterns.py` に集約し、
|
|
228
|
+
個別実装を禁止する。新しいパターンは PR + ReDoS テストを必須とする。
|
|
229
|
+
|
|
230
|
+
**ツール追加の必要性**: `ruff` の `W605`(invalid escape sequence)は有効。
|
|
231
|
+
ReDoS 専用ツール(`regexploit` / `rexploiter`)をCIに組み込む価値がある。
|
|
232
|
+
|
|
233
|
+
### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
|
|
234
|
+
|
|
235
|
+
**ポリシー達成度**: 中
|
|
236
|
+
|
|
237
|
+
**「初心者でも安全な API」達成度**: 中
|
|
238
|
+
- `re.fullmatch()` + コンパイル済みパターン + `{1,N}` 量化子制限の三点セットを規約化すれば達成できる
|
|
239
|
+
- 現時点でバリデーションパターンが散在しており、`nene2.validation.patterns` モジュールに集約する設計が必要
|
|
240
|
+
|
|
241
|
+
**設計上の負債・ドキュメント不足**:
|
|
242
|
+
- nene2 標準ライブラリにメール・URL・UUID バリデーション用コンパイル済みパターンがない
|
|
243
|
+
- ReDoS 危険パターンの禁止が CLAUDE.md に明文化されていない
|
|
244
|
+
|
|
245
|
+
**Follow-up Issue 候補**:
|
|
246
|
+
- `feat: nene2.validation.patterns — 安全な入力バリデーション用コンパイル済みパターン集を追加`
|
|
247
|
+
- `docs: CLAUDE.md にReDoS 禁止パターンのチェックリストを追記`
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## セキュリティ診断(FT168 % 3 = 0)
|
|
252
|
+
|
|
253
|
+
> **診断方針**: Django・FastAPI・SQLAlchemy 本体でも CVE が報告されてきたレベルの
|
|
254
|
+
> 攻撃ベクターを対象とする。「動いているから安全」は不正解。
|
|
255
|
+
> 実装ミスが起きやすい箇所を意図的に探し、問題がなければその理由まで記録する。
|
|
256
|
+
|
|
257
|
+
### 1. OWASP API Security Top 10 (2023)
|
|
258
|
+
|
|
259
|
+
#### API1: オブジェクトレベルの認可不備 (BOLA / IDOR)
|
|
260
|
+
- **結果**: ✅ 該当なし。FT168 はリソース所有権を持つエンドポイントなし。
|
|
261
|
+
`/re/search` 等は入力テキストを処理するユーティリティ型 API。
|
|
262
|
+
|
|
263
|
+
#### API2: 認証の破損 (Broken Authentication)
|
|
264
|
+
- **結果**: ✅ 該当なし。FT168 は認証なし(サンドボックス FT のため)。
|
|
265
|
+
nene2-python 本体の `ApiKeyAuthMiddleware` / `BearerTokenMiddleware` は別途テスト済み。
|
|
266
|
+
|
|
267
|
+
#### API3: オブジェクトプロパティレベルの認可不備 (Mass Assignment)
|
|
268
|
+
- `TransitionBody` 等で Pydantic BaseModel の extra フィールドを確認
|
|
269
|
+
- **結果**: ✅ Pydantic v2 デフォルト (`extra="ignore"`) により未知フィールドは無視される。
|
|
270
|
+
`{"pattern": "\\d+", "is_admin": true}` を POST しても `is_admin` は内部に届かない。
|
|
271
|
+
|
|
272
|
+
#### API4: 無制限リソース消費 (Unrestricted Resource Consumption)
|
|
273
|
+
- `text: str = Field(max_length=2000)` が全エンドポイントに設定済み
|
|
274
|
+
- `/re/redos-benchmark` は `max_length=100` + アプリレベルの長さチェックのダブルガード
|
|
275
|
+
- **結果**: ✅ 入力サイズ上限あり。
|
|
276
|
+
`"a" * 200` を送ると Pydantic が 422 で即時拒否(実際にテストで確認済み)。
|
|
277
|
+
|
|
278
|
+
#### API5: 機能レベルの認可不備 (Broken Function Level Authorization)
|
|
279
|
+
- **結果**: ✅ 管理者専用エンドポイントなし。
|
|
280
|
+
`/docs` / `/openapi.json` は開発 FT のためデフォルト有効(本番 APP_ENV での無効化は nene2 本体ポリシーで担保)。
|
|
281
|
+
|
|
282
|
+
#### API6: SSRF
|
|
283
|
+
- **結果**: ✅ 該当なし。外部 URL への HTTP 接続を行うエンドポイントなし。
|
|
284
|
+
|
|
285
|
+
#### API7: セキュリティの設定ミス
|
|
286
|
+
- `SecurityHeadersMiddleware` + `RequestIdMiddleware` が追加済み
|
|
287
|
+
- **結果**: ✅ `x-content-type-options`・`x-request-id` ヘッダーをテストで確認済み。
|
|
288
|
+
`APP_DEBUG=false`(デフォルト)でスタックトレース非公開。
|
|
289
|
+
|
|
290
|
+
#### API8: バージョン管理の欠落
|
|
291
|
+
- **結果**: ✅ 非推奨エンドポイントなし。
|
|
292
|
+
|
|
293
|
+
#### API9: 不適切な在庫管理
|
|
294
|
+
- **結果**: ✅ ハードコードされたシークレットなし。
|
|
295
|
+
`grep -rn "secret\|password\|api_key" ft168-re/` でヒットなし。
|
|
296
|
+
|
|
297
|
+
#### API10: 安全でない API の消費
|
|
298
|
+
- **結果**: ✅ 外部 API 消費なし。
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
### 2. インジェクション攻撃
|
|
303
|
+
|
|
304
|
+
#### SQL インジェクション
|
|
305
|
+
- **結果**: ✅ 該当なし。FT168 は DB を使用しない。
|
|
306
|
+
nene2-python 本体のリポジトリ層は SQLAlchemy ORM + パラメータ化クエリのみ使用(ソースコード確認済み)。
|
|
307
|
+
|
|
308
|
+
#### コマンドインジェクション
|
|
309
|
+
- `grep -rn "subprocess\|os.system\|shell=True" ft168-re/` → ヒットなし
|
|
310
|
+
- **結果**: ✅ コマンド実行コードなし。
|
|
311
|
+
|
|
312
|
+
#### パストラバーサル
|
|
313
|
+
- **結果**: ✅ 該当なし。ファイル操作なし。
|
|
314
|
+
|
|
315
|
+
#### SSTI(サーバーサイドテンプレートインジェクション)
|
|
316
|
+
- Jinja2 テンプレート使用なし
|
|
317
|
+
- **結果**: ✅ `{{7*7}}` を送っても JSONResponse では評価されない(文字列として返却)。
|
|
318
|
+
|
|
319
|
+
#### 正規表現インジェクション(re モジュール固有)
|
|
320
|
+
- `/re/sub` エンドポイントはユーザー指定パターンを受け付ける
|
|
321
|
+
- `re.error` で不正パターン(`[invalid`)を捕捉して 422 を返すことをテストで確認
|
|
322
|
+
- `re.escape()` を使う `split_on_delimiters()` / `highlight_keyword()` はメタ文字を無効化
|
|
323
|
+
- **結果**: ✅ ユーザー入力をパターンとして使う箇所は `re.error` 捕捉済み。
|
|
324
|
+
ただし `/re/sub` は任意パターン受付のため、悪意ある ReDoS パターンを送られうる → 次項参照。
|
|
325
|
+
|
|
326
|
+
#### HTTP ヘッダーインジェクション
|
|
327
|
+
- **結果**: ✅ レスポンスヘッダーにユーザー入力を直接セットしていない。
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
### 3. 認証・認可
|
|
332
|
+
- **結果**: ✅ 認証機能なし(FT サンドボックスのため)。
|
|
333
|
+
nene2 本体の `secrets.compare_digest()` 使用は FT165 で検証済み。
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
### 4. 入力バリデーション
|
|
338
|
+
|
|
339
|
+
- すべての Query / Body パラメータに `max_length` あり
|
|
340
|
+
- `RedosRequest.input: str = Field(max_length=100)` で ReDoS 攻撃の入力長を制限
|
|
341
|
+
|
|
342
|
+
Null バイトテスト:
|
|
343
|
+
```python
|
|
344
|
+
client.post("/re/sub", json={"pattern": r"\x00", "replacement": "", "text": "\x00evil"})
|
|
345
|
+
# → 200 (re は \x00 を処理できる。DB 書き込みなし)
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
- **結果**: ✅ 全 HTTP 境界に型 + 長さバリデーション済み。
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
### 5. 情報漏洩
|
|
353
|
+
|
|
354
|
+
- `APP_DEBUG=false`(デフォルト)時: `ErrorHandlerMiddleware` がスタックトレースを除去
|
|
355
|
+
- `pip-audit` 結果: PyJWT 2.12.1 の PYSEC-2025-183 が `mcp` 経由で存在(FT165 で記録済み、修正不可)
|
|
356
|
+
- **結果**: ⚠️ PYSEC-2025-183(PyJWT)が継続中(FT165 で記録済み、対処方針は変更なし)。
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
### 6. Python / FastAPI 固有の攻撃ベクター
|
|
361
|
+
|
|
362
|
+
#### ReDoS(Regular Expression DoS)— **FT168 の重点診断項目**
|
|
363
|
+
|
|
364
|
+
**実測値(Python 3.14.5 / WSL2)**:
|
|
365
|
+
|
|
366
|
+
| 入力長 | 安全パターン `^a+$` | 脆弱パターン `^(a+)+$` |
|
|
367
|
+
|---|---|---|
|
|
368
|
+
| 20 | 0.002 ms | 37 ms |
|
|
369
|
+
| 25 | 0.004 ms | 1,247 ms |
|
|
370
|
+
| 28 | 0.003 ms | 10,369 ms |
|
|
371
|
+
| 30 | < 0.01 ms | > 2,000 ms(SIGALRM timeout) |
|
|
372
|
+
|
|
373
|
+
**発見: `^(a+)+$` は Python 3.14.5 でも指数時間**
|
|
374
|
+
Python 3.11 で re エンジンは一部改善されたが、`(a+)+` のような nested quantifier は
|
|
375
|
+
Python 3.14.5 時点でも依然として指数的バックトラッキングが発生する。
|
|
376
|
+
n=30 の "aaa...ab" で SIGALRM 2 秒 timeout が確認された。
|
|
377
|
+
|
|
378
|
+
**防御策(実装済み)**:
|
|
379
|
+
1. `max_length=100` で Pydantic がリクエスト時点で 422 を返す(ガードを外せばサーバーが停止)
|
|
380
|
+
2. `SAFE_PATTERN = re.compile(r"^a+$")` は同等だが O(n) で動作
|
|
381
|
+
|
|
382
|
+
**注意**: `/re/sub` エンドポイントはユーザーが任意パターンを指定できる。
|
|
383
|
+
`re.error` の捕捉はあるが、`^(a+)+$` のような文法的に正しい ReDoS パターンは通過する。
|
|
384
|
+
現状は `max_length=200` でパターン長を制限しているが、短いパターンでも ReDoS は発生しうる。
|
|
385
|
+
**本番で任意パターン受付 API を作る場合は `re.timeout` 相当の対策が必須**(Python 標準にはない。`signal.alarm` や別スレッドで対処)。
|
|
386
|
+
|
|
387
|
+
- **結果**: ⚠️ FT168 サンドボックスは `max_length` で緩和済み。
|
|
388
|
+
任意パターン受付 API への本番対応パターンは未確立 → Issue 化推奨。
|
|
389
|
+
|
|
390
|
+
#### pickle / yaml
|
|
391
|
+
- `grep -rn "pickle\|yaml\.load\|marshal" ft168-re/` → ヒットなし
|
|
392
|
+
- **結果**: ✅
|
|
393
|
+
|
|
394
|
+
#### 非同期レースコンディション
|
|
395
|
+
- グローバル変更なし(すべての関数が純粋関数)
|
|
396
|
+
- **結果**: ✅
|
|
397
|
+
|
|
398
|
+
#### 型強制攻撃 (Pydantic Type Coercion)
|
|
399
|
+
- `bool` フィールドへの `"yes"` / `1` 送信を実測: `True` に変換される(Pydantic v2 デフォルト)
|
|
400
|
+
- FT168 の `RedosRequest` には `bool` フィールドなし
|
|
401
|
+
- **影響なし**。ただしセキュリティ判定フィールド(`is_admin` 等)に `bool` を使う場合は
|
|
402
|
+
`ConfigDict(strict=True)` を推奨する。
|
|
403
|
+
- **結果**: ✅ FT168 スコープでは影響なし。
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
### 7. 依存関係の脆弱性スキャン
|
|
408
|
+
|
|
409
|
+
nene2-python 本体で `uv run pip-audit` 実行:
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
Found 1 known vulnerability in 1 package
|
|
413
|
+
Name Version ID Fix Versions
|
|
414
|
+
----- ------- -------------- ------------
|
|
415
|
+
pyjwt 2.12.1 PYSEC-2025-183
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
- **スキャン結果**: 1件(FT165 から継続)
|
|
419
|
+
- **対応方針**: `mcp>=1.0` が pyjwt を推移的依存として引き込んでいる。`mcp` の修正を待つ。
|
|
420
|
+
nene2-python の直接依存からは FT165 で除去済み。nene2 のコードは pyjwt を直接使用していない。
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
### 診断サマリー
|
|
425
|
+
|
|
426
|
+
| カテゴリ | 結果 | 最重要発見 |
|
|
427
|
+
|---|---|---|
|
|
428
|
+
| OWASP API Security Top 10 | ✅ 全通過 | 該当なし |
|
|
429
|
+
| SQL インジェクション | ✅ | 該当なし |
|
|
430
|
+
| コマンドインジェクション | ✅ | 該当なし |
|
|
431
|
+
| パストラバーサル | ✅ | 該当なし |
|
|
432
|
+
| SSTI | ✅ | 該当なし |
|
|
433
|
+
| 正規表現インジェクション | ✅ | `re.error` 捕捉済み、`re.escape()` 使用済み |
|
|
434
|
+
| 認証・認可 | ✅ | FT サンドボックスのため該当なし |
|
|
435
|
+
| 入力バリデーション | ✅ | 全境界に `max_length` あり |
|
|
436
|
+
| 情報漏洩 | ⚠️ | PyJWT PYSEC-2025-183(mcp 経由・継続中) |
|
|
437
|
+
| **ReDoS** | ⚠️ | `(a+)+` が Python 3.14 でも指数時間(n=30 で 2秒+ timeout) |
|
|
438
|
+
| pickle / yaml | ✅ | 該当なし |
|
|
439
|
+
| 非同期レースコンディション | ✅ | 純粋関数のみ |
|
|
440
|
+
| 型強制攻撃 | ✅ | FT168 スコープでは影響なし |
|
|
441
|
+
| 依存関係 CVE | ⚠️ | PYSEC-2025-183(継続中) |
|
|
442
|
+
|
|
443
|
+
**総合評価**: 条件付き合格(ReDoS の本番対応パターンが未確立)
|
|
444
|
+
**発見した脆弱性**: 1件(任意パターン受付 API への ReDoS 対策が未確立)
|
|
445
|
+
**セキュリティ観察**: MEDIUM 1件(ReDoS)、INFO 1件(PyJWT 継続)
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Follow-up Issues
|
|
450
|
+
|
|
451
|
+
| 優先度 | タイトル | 種別 |
|
|
452
|
+
|---|---|---|
|
|
453
|
+
| 高 | `feat: nene2.validation.patterns — 安全なバリデーション用コンパイル済みパターン集` | feat |
|
|
454
|
+
| 中 | `docs: CLAUDE.md に ReDoS 禁止パターンのチェックリストと signal.alarm 対策を追記` | docs |
|
|
455
|
+
| 中 | `docs: 任意パターン受付 API への ReDoS タイムアウト対策 how-to を追加` | docs |
|
|
456
|
+
| 低 | `fix: /re/sub の任意パターン受付に signal.alarm タイムアウトガードを追加` | fix |
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## まとめ
|
|
461
|
+
|
|
462
|
+
`re` モジュールは nene2-python の入力バリデーション・ログ解析・テキスト変換で直接使える重要機能。
|
|
463
|
+
46 テスト全通過(摩擦1件は期待値の記述ミス、修正済み)。
|
|
464
|
+
|
|
465
|
+
**セキュリティ診断の主な発見**: ReDoS は Python 3.14.5 でも実在する脅威であることを実測で確認した。
|
|
466
|
+
`^(a+)+$` パターンで n=30 の入力に 2 秒以上かかる(タイムアウト)。
|
|
467
|
+
FT168 サンドボックスでは `max_length` 制限で緩和しているが、
|
|
468
|
+
任意パターンを受け付ける API に対する本番品質の対策パターン(`signal.alarm` + スレッド分離)の
|
|
469
|
+
ドキュメント化が次の課題。
|
|
470
|
+
|
|
471
|
+
`re.escape()` による正規表現インジェクション防止・量化子の上限制限・`re.fullmatch()` の徹底使用が
|
|
472
|
+
nene2-python の正規表現 3 原則として確立できる。
|
|
473
|
+
|