Add S3/MinIO storage, nelmio security and CSP config

- Flysystem S3 adapter configured for MinIO
- Vich uploader switched to Flysystem S3 storage
- Liip imagine loader/resolver on S3
- S3 client service with path style endpoint for MinIO
- Nelmio security: CSP, clickjacking, permissions policy, external redirects
- CSP dev: allow Vite HMR (localhost:5173)
- CSP prod: nonce scripts, restricted form-action and connect-src
- composer: flysystem-bundle, flysystem-aws-s3-v3, nelmio/security-bundle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-18 21:10:45 +01:00
parent e3de0da1bf
commit 2d02ba4cbb
13 changed files with 1664 additions and 3 deletions

8
.env
View File

@@ -46,3 +46,11 @@ STRIPE_SK=
STRIPE_WEBHOOK_SECRET= STRIPE_WEBHOOK_SECRET=
STRIPE_MODE=test STRIPE_MODE=test
SMIME_PASSPHRASE='KLreLnyR07x5h#3$AC' SMIME_PASSPHRASE='KLreLnyR07x5h#3$AC'
###> s3/minio ###
S3_ENDPOINT=http://minio:9000
S3_ACCESS_KEY=e-ticket
S3_SECRET_KEY=e-ticket
S3_BUCKET=e-ticket
S3_REGION=us-east-1
###< s3/minio ###

View File

@@ -12,7 +12,10 @@
"doctrine/orm": "^3.6", "doctrine/orm": "^3.6",
"dompdf/dompdf": "*", "dompdf/dompdf": "*",
"endroid/qr-code-bundle": "*", "endroid/qr-code-bundle": "*",
"league/flysystem-aws-s3-v3": "^3.32",
"league/flysystem-bundle": "^3.6",
"liip/imagine-bundle": "^2.17", "liip/imagine-bundle": "^2.17",
"nelmio/security-bundle": "^3.9",
"phpdocumentor/reflection-docblock": "^6.0", "phpdocumentor/reflection-docblock": "^6.0",
"phpstan/phpdoc-parser": "^2.3", "phpstan/phpdoc-parser": "^2.3",
"symfony/asset": "8.0.*", "symfony/asset": "8.0.*",

1271
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,4 +14,6 @@ return [
Endroid\QrCodeBundle\EndroidQrCodeBundle::class => ['all' => true], Endroid\QrCodeBundle\EndroidQrCodeBundle::class => ['all' => true],
Liip\ImagineBundle\LiipImagineBundle::class => ['all' => true], Liip\ImagineBundle\LiipImagineBundle::class => ['all' => true],
Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true], Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true],
League\FlysystemBundle\FlysystemBundle::class => ['all' => true],
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
]; ];

View File

@@ -0,0 +1,10 @@
nelmio_security:
csp:
enforce:
script-src:
- 'wasm-unsafe-eval'
- 'http://localhost:5173/'
style-src:
- 'unsafe-inline'
connect-src:
- 'ws://localhost:5173'

View File

@@ -0,0 +1,8 @@
flysystem:
storages:
default.storage:
adapter: 'aws'
options:
client: 's3_client'
bucket: '%env(S3_BUCKET)%'
prefix: 'uploads'

View File

@@ -1,6 +1,22 @@
liip_imagine: liip_imagine:
driver: imagick driver: imagick
loaders:
flysystem_loader:
flysystem:
filesystem_service: default.storage
data_loader: flysystem_loader
resolvers:
flysystem_resolver:
flysystem:
filesystem_service: default.storage
root_url: '%env(S3_ENDPOINT)%/%env(S3_BUCKET)%'
cache_prefix: cache
cache: flysystem_resolver
webp: webp:
generate: true generate: true
quality: 80 quality: 80

View File

@@ -0,0 +1,73 @@
nelmio_security:
clickjacking:
paths:
'^/.*': DENY
content_type:
nosniff: true
referrer_policy:
enabled: true
policies:
- 'no-referrer'
- 'strict-origin-when-cross-origin'
csp:
enforce:
level1_fallback: false
browser_adaptive:
enabled: false
report-uri: '%router.request_context.base_url%/my-csp-report'
frame-ancestors:
- 'none'
frame-src:
- 'https://stripe.com'
- 'https://*.stripe.com'
- 'https://js.stripe.com'
- 'https://cloudflare.com'
- 'https://*.cloudflareinsights.com'
script-src:
- 'self'
- 'https://static.cloudflareinsights.com'
style-src:
- 'self'
- 'https://fonts.googleapis.com'
- 'https://cdnjs.cloudflare.com'
img-src:
- 'self'
- 'data:'
worker-src:
- 'self'
- 'blob:'
connect-src:
- 'self'
- 'https://cloudflareinsights.com'
- 'https://static.cloudflareinsights.com'
font-src:
- 'self'
- 'https://cdnjs.cloudflare.com'
- 'https://fonts.googleapis.com'
- 'https://fonts.gstatic.com'
object-src:
- 'none'
block-all-mixed-content: true
permissions_policy:
enabled: true
policies:
payment: ['self']
camera: ['self']
microphone: []
geolocation: ['self']
external_redirects:
override: /external-redirect
forward_as: redirUrl
log: true
allow_list:
- cloudflareinsights.com
- static.cloudflareinsights.com
- stripe.com
- checkout.stripe.com
- hooks.stripe.com

View File

@@ -0,0 +1,21 @@
nelmio_security:
csp:
enforce:
script-src:
- 'self'
- 'nonce'
- 'https://static.cloudflareinsights.com'
# Restreindre les soumissions de formulaires à notre domaine
# et aux redirections OAuth des plateformes de partage social
form-action:
- 'self'
- 'https://www.facebook.com'
- 'https://x.com'
- 'https://twitter.com'
# Autoriser navigator.share() (Web Share API) et clipboard API
# — les deux sont des APIs navigateur natives, pas des appels réseau externes
# Ce bloc est présent pour documentation et futures intégrations
connect-src:
- 'self'

View File

@@ -2,9 +2,14 @@ vich_uploader:
db_driver: orm db_driver: orm
metadata: metadata:
type: attribute type: attribute
storage: flysystem
mappings: mappings:
event_image: event_image:
uri_prefix: /uploads/events uri_prefix: '%env(S3_ENDPOINT)%/%env(S3_BUCKET)%/uploads/events'
upload_destination: '%kernel.project_dir%/public/uploads/events' upload_destination: default.storage
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
directory_namer:
service: Vich\UploaderBundle\Naming\CurrentDateDirectoryNamer
options:
date_time_format: 'Y/m'

View File

@@ -1581,6 +1581,207 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* db_driver?: scalar|Param|null, // Default: null * db_driver?: scalar|Param|null, // Default: null
* }>, * }>,
* } * }
* @psalm-type FlysystemConfig = array{
* storages?: array<string, array{ // Default: []
* adapter: scalar|Param|null,
* options?: list<mixed>,
* visibility?: scalar|Param|null, // Default: null
* directory_visibility?: scalar|Param|null, // Default: null
* retain_visibility?: bool|Param|null, // Default: null
* case_sensitive?: bool|Param, // Default: true
* disable_asserts?: bool|Param, // Default: false
* public_url?: list<scalar|Param|null>,
* path_normalizer?: scalar|Param|null, // Default: null
* public_url_generator?: scalar|Param|null, // Default: null
* temporary_url_generator?: scalar|Param|null, // Default: null
* read_only?: bool|Param, // Default: false
* }>,
* }
* @psalm-type NelmioSecurityConfig = array{
* signed_cookie?: array{
* names?: list<scalar|Param|null>,
* secret?: scalar|Param|null, // Default: "%kernel.secret%"
* hash_algo?: scalar|Param|null,
* legacy_hash_algo?: scalar|Param|null, // Fallback algorithm to allow for frictionless hash algorithm upgrades. Use with caution and as a temporary measure as it allows for downgrade attacks. // Default: null
* separator?: scalar|Param|null, // Default: "."
* },
* clickjacking?: array{
* hosts?: list<scalar|Param|null>,
* paths?: array<string, array{ // Default: {"^/.*":{"header":"DENY"}}
* header?: scalar|Param|null, // Default: "DENY"
* }>,
* content_types?: list<scalar|Param|null>,
* },
* external_redirects?: array{
* abort?: bool|Param, // Default: false
* override?: scalar|Param|null, // Default: null
* forward_as?: scalar|Param|null, // Default: null
* log?: bool|Param, // Default: false
* allow_list?: list<scalar|Param|null>,
* },
* flexible_ssl?: bool|array{
* enabled?: bool|Param, // Default: false
* cookie_name?: scalar|Param|null, // Default: "auth"
* unsecured_logout?: bool|Param, // Default: false
* },
* forced_ssl?: bool|array{
* enabled?: bool|Param, // Default: false
* hsts_max_age?: scalar|Param|null, // Default: null
* hsts_subdomains?: bool|Param, // Default: false
* hsts_preload?: bool|Param, // Default: false
* allow_list?: list<scalar|Param|null>,
* hosts?: list<scalar|Param|null>,
* redirect_status_code?: scalar|Param|null, // Default: 302
* },
* content_type?: array{
* nosniff?: bool|Param, // Default: false
* },
* xss_protection?: array{ // Deprecated: The "xss_protection" option is deprecated, use Content Security Policy without allowing "unsafe-inline" scripts instead.
* enabled?: bool|Param, // Default: false
* mode_block?: bool|Param, // Default: false
* report_uri?: scalar|Param|null, // Default: null
* },
* csp?: bool|array{
* enabled?: bool|Param, // Default: true
* request_matcher?: scalar|Param|null, // Default: null
* hosts?: list<scalar|Param|null>,
* content_types?: list<scalar|Param|null>,
* report_endpoint?: array{
* log_channel?: scalar|Param|null, // Default: null
* log_formatter?: scalar|Param|null, // Default: "nelmio_security.csp_report.log_formatter"
* log_level?: "alert"|"critical"|"debug"|"emergency"|"error"|"info"|"notice"|"warning"|Param, // Default: "notice"
* filters?: array{
* domains?: bool|Param, // Default: true
* schemes?: bool|Param, // Default: true
* browser_bugs?: bool|Param, // Default: true
* injected_scripts?: bool|Param, // Default: true
* },
* dismiss?: list<list<"default-src"|"base-uri"|"block-all-mixed-content"|"child-src"|"connect-src"|"font-src"|"form-action"|"frame-ancestors"|"frame-src"|"img-src"|"manifest-src"|"media-src"|"object-src"|"plugin-types"|"script-src"|"style-src"|"upgrade-insecure-requests"|"report-uri"|"worker-src"|"prefetch-src"|"report-to"|"*"|Param>>,
* },
* compat_headers?: bool|Param, // Default: true
* report_logger_service?: scalar|Param|null, // Default: "logger"
* hash?: array{
* algorithm?: "sha256"|"sha384"|"sha512"|Param, // The algorithm to use for hashes // Default: "sha256"
* },
* report?: array{
* level1_fallback?: bool|Param, // Provides CSP Level 1 fallback when using hash or nonce (CSP level 2) by adding 'unsafe-inline' source. See https://www.w3.org/TR/CSP2/#directive-script-src and https://www.w3.org/TR/CSP2/#directive-style-src // Default: true
* browser_adaptive?: bool|array{ // Do not send directives that browser do not support
* enabled?: bool|Param, // Default: false
* parser?: scalar|Param|null, // Default: "nelmio_security.ua_parser.ua_php"
* },
* default-src?: list<scalar|Param|null>,
* base-uri?: list<scalar|Param|null>,
* block-all-mixed-content?: bool|Param, // Default: false
* child-src?: list<scalar|Param|null>,
* connect-src?: list<scalar|Param|null>,
* font-src?: list<scalar|Param|null>,
* form-action?: list<scalar|Param|null>,
* frame-ancestors?: list<scalar|Param|null>,
* frame-src?: list<scalar|Param|null>,
* img-src?: list<scalar|Param|null>,
* manifest-src?: list<scalar|Param|null>,
* media-src?: list<scalar|Param|null>,
* object-src?: list<scalar|Param|null>,
* plugin-types?: list<scalar|Param|null>,
* script-src?: list<scalar|Param|null>,
* style-src?: list<scalar|Param|null>,
* upgrade-insecure-requests?: bool|Param, // Default: false
* report-uri?: list<scalar|Param|null>,
* worker-src?: list<scalar|Param|null>,
* prefetch-src?: list<scalar|Param|null>,
* report-to?: scalar|Param|null,
* },
* enforce?: array{
* level1_fallback?: bool|Param, // Provides CSP Level 1 fallback when using hash or nonce (CSP level 2) by adding 'unsafe-inline' source. See https://www.w3.org/TR/CSP2/#directive-script-src and https://www.w3.org/TR/CSP2/#directive-style-src // Default: true
* browser_adaptive?: bool|array{ // Do not send directives that browser do not support
* enabled?: bool|Param, // Default: false
* parser?: scalar|Param|null, // Default: "nelmio_security.ua_parser.ua_php"
* },
* default-src?: list<scalar|Param|null>,
* base-uri?: list<scalar|Param|null>,
* block-all-mixed-content?: bool|Param, // Default: false
* child-src?: list<scalar|Param|null>,
* connect-src?: list<scalar|Param|null>,
* font-src?: list<scalar|Param|null>,
* form-action?: list<scalar|Param|null>,
* frame-ancestors?: list<scalar|Param|null>,
* frame-src?: list<scalar|Param|null>,
* img-src?: list<scalar|Param|null>,
* manifest-src?: list<scalar|Param|null>,
* media-src?: list<scalar|Param|null>,
* object-src?: list<scalar|Param|null>,
* plugin-types?: list<scalar|Param|null>,
* script-src?: list<scalar|Param|null>,
* style-src?: list<scalar|Param|null>,
* upgrade-insecure-requests?: bool|Param, // Default: false
* report-uri?: list<scalar|Param|null>,
* worker-src?: list<scalar|Param|null>,
* prefetch-src?: list<scalar|Param|null>,
* report-to?: scalar|Param|null,
* },
* },
* referrer_policy?: bool|array{
* enabled?: bool|Param, // Default: false
* policies?: list<scalar|Param|null>,
* },
* permissions_policy?: bool|array{
* enabled?: bool|Param, // Default: false
* policies?: array{
* accelerometer?: mixed, // Default: null
* ambient_light_sensor?: mixed, // Default: null
* attribution_reporting?: mixed, // Default: null
* autoplay?: mixed, // Default: null
* bluetooth?: mixed, // Default: null
* browsing_topics?: mixed, // Default: null
* camera?: mixed, // Default: null
* captured_surface_control?: mixed, // Default: null
* compute_pressure?: mixed, // Default: null
* cross_origin_isolated?: mixed, // Default: null
* deferred_fetch?: mixed, // Default: null
* deferred_fetch_minimal?: mixed, // Default: null
* display_capture?: mixed, // Default: null
* encrypted_media?: mixed, // Default: null
* fullscreen?: mixed, // Default: null
* gamepad?: mixed, // Default: null
* geolocation?: mixed, // Default: null
* gyroscope?: mixed, // Default: null
* hid?: mixed, // Default: null
* identity_credentials_get?: mixed, // Default: null
* idle_detection?: mixed, // Default: null
* interest_cohort?: mixed, // Default: null
* language_detector?: mixed, // Default: null
* local_fonts?: mixed, // Default: null
* magnetometer?: mixed, // Default: null
* microphone?: mixed, // Default: null
* midi?: mixed, // Default: null
* otp_credentials?: mixed, // Default: null
* payment?: mixed, // Default: null
* picture_in_picture?: mixed, // Default: null
* publickey_credentials_create?: mixed, // Default: null
* publickey_credentials_get?: mixed, // Default: null
* screen_wake_lock?: mixed, // Default: null
* serial?: mixed, // Default: null
* speaker_selection?: mixed, // Default: null
* storage_access?: mixed, // Default: null
* summarizer?: mixed, // Default: null
* translator?: mixed, // Default: null
* usb?: mixed, // Default: null
* web_share?: mixed, // Default: null
* window_management?: mixed, // Default: null
* xr_spatial_tracking?: mixed, // Default: null
* },
* },
* cross_origin_isolation?: bool|array{
* enabled?: bool|Param, // Default: false
* paths?: array<string, array{ // Default: []
* coep?: "unsafe-none"|"require-corp"|"credentialless"|Param, // Cross-Origin-Embedder-Policy (COEP) header value
* coop?: "unsafe-none"|"same-origin-allow-popups"|"same-origin"|"noopener-allow-popups"|Param, // Cross-Origin-Opener-Policy (COOP) header value
* corp?: "same-site"|"same-origin"|"cross-origin"|Param, // Cross-Origin-Resource-Policy (CORP) header value
* report_only?: bool|Param, // Use Report-Only headers instead of enforcing (applies to COEP and COOP only) // Default: false
* report_to?: scalar|Param|null, // Reporting endpoint name for violations (requires Reporting API configuration, applies to COEP and COOP only) // Default: null
* }>,
* },
* }
* @psalm-type ConfigType = array{ * @psalm-type ConfigType = array{
* imports?: ImportsConfig, * imports?: ImportsConfig,
* parameters?: ParametersConfig, * parameters?: ParametersConfig,
@@ -1595,6 +1796,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* endroid_qr_code?: EndroidQrCodeConfig, * endroid_qr_code?: EndroidQrCodeConfig,
* liip_imagine?: LiipImagineConfig, * liip_imagine?: LiipImagineConfig,
* vich_uploader?: VichUploaderConfig, * vich_uploader?: VichUploaderConfig,
* flysystem?: FlysystemConfig,
* nelmio_security?: NelmioSecurityConfig,
* "when@dev"?: array{ * "when@dev"?: array{
* imports?: ImportsConfig, * imports?: ImportsConfig,
* parameters?: ParametersConfig, * parameters?: ParametersConfig,
@@ -1612,6 +1815,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* endroid_qr_code?: EndroidQrCodeConfig, * endroid_qr_code?: EndroidQrCodeConfig,
* liip_imagine?: LiipImagineConfig, * liip_imagine?: LiipImagineConfig,
* vich_uploader?: VichUploaderConfig, * vich_uploader?: VichUploaderConfig,
* flysystem?: FlysystemConfig,
* nelmio_security?: NelmioSecurityConfig,
* }, * },
* "when@prod"?: array{ * "when@prod"?: array{
* imports?: ImportsConfig, * imports?: ImportsConfig,
@@ -1627,6 +1832,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* endroid_qr_code?: EndroidQrCodeConfig, * endroid_qr_code?: EndroidQrCodeConfig,
* liip_imagine?: LiipImagineConfig, * liip_imagine?: LiipImagineConfig,
* vich_uploader?: VichUploaderConfig, * vich_uploader?: VichUploaderConfig,
* flysystem?: FlysystemConfig,
* nelmio_security?: NelmioSecurityConfig,
* }, * },
* "when@test"?: array{ * "when@test"?: array{
* imports?: ImportsConfig, * imports?: ImportsConfig,
@@ -1643,6 +1850,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* endroid_qr_code?: EndroidQrCodeConfig, * endroid_qr_code?: EndroidQrCodeConfig,
* liip_imagine?: LiipImagineConfig, * liip_imagine?: LiipImagineConfig,
* vich_uploader?: VichUploaderConfig, * vich_uploader?: VichUploaderConfig,
* flysystem?: FlysystemConfig,
* nelmio_security?: NelmioSecurityConfig,
* }, * },
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias * ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
* imports?: ImportsConfig, * imports?: ImportsConfig,

View File

@@ -22,6 +22,18 @@ services:
# add more service definitions when explicit configuration is needed # add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones # please note that last definitions always *replace* previous ones
s3_client:
class: Aws\S3\S3Client
arguments:
-
version: 'latest'
region: '%env(S3_REGION)%'
endpoint: '%env(S3_ENDPOINT)%'
use_path_style_endpoint: true
credentials:
key: '%env(S3_ACCESS_KEY)%'
secret: '%env(S3_SECRET_KEY)%'
App\Twig\ViteAssetExtension: App\Twig\ViteAssetExtension:
arguments: arguments:
$manifest: '%kernel.project_dir%/public/build/.vite/manifest.json' $manifest: '%kernel.project_dir%/public/build/.vite/manifest.json'

View File

@@ -38,6 +38,19 @@
"endroid/qr-code-bundle": { "endroid/qr-code-bundle": {
"version": "6.1.0" "version": "6.1.0"
}, },
"league/flysystem-bundle": {
"version": "3.6",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "913dc3d7a5a1af0d2b044c5ac3a16e2f851d7380"
},
"files": [
"config/packages/flysystem.yaml",
"var/storage/.gitignore"
]
},
"liip/imagine-bundle": { "liip/imagine-bundle": {
"version": "2.17", "version": "2.17",
"recipe": { "recipe": {
@@ -47,6 +60,18 @@
"ref": "d1227d002b70d1a1f941d91845fcd7ac7fbfc929" "ref": "d1227d002b70d1a1f941d91845fcd7ac7fbfc929"
} }
}, },
"nelmio/security-bundle": {
"version": "3.9",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.4",
"ref": "71045833e4f882ad9de8c95fe47efb99a1eec2f7"
},
"files": [
"config/packages/nelmio_security.yaml"
]
},
"phpunit/phpunit": { "phpunit/phpunit": {
"version": "13.0", "version": "13.0",
"recipe": { "recipe": {