Block requests with specific query string parameters

1. The problem I’m having:

Hi! I’m trying to block some requests to a reverse proxied backend (emby) with a particular query string parameters. In Nginx I was able to do it with the following “if’s” inside the server block:

if ( $arg_X-Emby-Client = "Emby%20for%20Android" ) {
	return 403;
}

if ( $arg_X-Emby-Client = "Emby%20for%20iOS" ) {
	return 403;
}

So with those “if” I was able to deny access to that particular request’s query strings. I’ve tried the followinf Caddyfile configs without success and I’m little stuck now:

Option 1:

somedomain {
	encode zstd gzip

	@blockapp {
		query X-Emby-Client=Emby%20for%20Android
		query X-Emby-Client=Emby%20for%20iOS
	}

	handle @blockapp {
		abort
	}

	reverse_proxy :8090
}

Option 2:

somedomain {
	encode zstd gzip

	@blockapp {
		query X-Emby-Client=Emby%20for%20Android
		query X-Emby-Client=Emby%20for%20iOS
	}

	abort @blockapp

	reverse_proxy :8090
}

None of them blocks the query strings I need.

2. Error messages and/or full log output:

No errors to show

3. Caddy version:

v2.6.4

4. How I installed and ran Caddy:

a. System environment:

Arch Linux, repo package, systemd

b. Command:

/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile

c. Service/unit/compose file:

# /usr/lib/systemd/system/caddy.service
# caddy.service
#
# For using Caddy with a config file.
#
# Make sure the ExecStart and ExecReload commands are correct
# for your installation.
#
# See https://caddyserver.com/docs/install for instructions.
#
# WARNING: This service does not use the --resume flag, so if you
# use the API to make changes, they will be overwritten by the
# Caddyfile next time the service is restarted. If you intend to
# use Caddy's API to configure it, add the --resume flag to the
# `caddy run` command or use the caddy-api.service file instead.

[Unit]
Description=Caddy web server
Documentation=https://caddyserver.com/docs/
After=network-online.target
Wants=network-online.target systemd-networkd-wait-online.service
StartLimitIntervalSec=14400
StartLimitBurst=10

[Service]
Type=notify
User=caddy
Group=caddy
Environment=XDG_DATA_HOME=/var/lib
Environment=XDG_CONFIG_HOME=/etc
ExecStartPre=/usr/bin/caddy validate --config /etc/caddy/Caddyfile
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
ExecStopPost=/usr/bin/rm -f /run/caddy/admin.socket

# Do not allow the process to be restarted in a tight loop. If the
# process fails to start, something critical needs to be fixed.
Restart=on-abnormal

# Use graceful shutdown with a reasonable timeout
TimeoutStopSec=5s

LimitNOFILE=1048576
LimitNPROC=512

# Hardening options
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
DevicePolicy=closed
LockPersonality=true
MemoryAccounting=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
ProcSubset=pid
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
ProtectSystem=strict
RemoveIPC=true
ReadWritePaths=/var/lib/caddy /var/log/caddy /run/caddy
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true

[Install]
WantedBy=multi-user.target

d. My complete Caddy config:

Shown above

5. Links to relevant resources:

N/A

How are you testing it? Show an example curl -v command.

My guess is it might have to do with URL encoding. Can you try it without the % characters to see if it works for you in that case (i.e. exact match)? We might be comparing against the un-encoded value instead.

1 Like

Here you have the Caddy log:

{"level":"info","ts":1690509977.8118727,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"someipaddress","remote_port":"49784","proto":"HTTP/3.0","method":"POST","host":"somedomain","uri":"/emby/Users/authenticatebyname?X-Emby-Client=Emby%20for%20Android&X-Emby-Device-Name=POCO%20X3%20NFC&X-Emby-Device-Id=someid&X-Emby-Client-Version=3.3.07&X-Emby-Language=es","headers":{"Sec-Fetch-Dest":["empty"],"Accept-Language":["es-ES,es;q=0.9,en-US;q=0.8,en;q=0.7"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; M2007J20CG Build/TQ3A.230605.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/114.0.5735.196 Mobile Safari/537.36"],"Content-Type":["application/x-www-form-urlencoded; charset=UTF-8"],"Sec-Fetch-Site":["cross-site"],"Sec-Fetch-Mode":["cors"],"Accept":["application/json"],"X-Requested-With":["com.mb.android"],"Accept-Encoding":["gzip, deflate"]},"tls":{"resumed":true,"version":772,"cipher_suite":4865,"proto":"h3","server_name":"somedomain"}},"user_id":"","duration":0.025381911,"size":1572,"status":200,"resp_headers":{"Access-Control-Allow-Methods":["GET, POST, PUT, DELETE, PATCH, OPTIONS"],"Private-Network-Access-Name":["Somedomain"],"Server":["Caddy"],"Access-Control-Allow-Headers":["Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Token, X-Emby-Client, X-Emby-Client-Version, X-Emby-Device-Id, X-Emby-Device-Name, X-Emby-Authorization"],"Access-Control-Allow-Private-Network":["true"],"Content-Type":["application/json; charset=utf-8"],"Access-Control-Allow-Origin":["*"],"Date":["Fri, 28 Jul 2023 02:06:17 GMT"],"X-Xss-Protection":["1; mode=block"],"Expires":["-1"],"Private-Network-Access-Id":["someid"],"Permissions-Policy":["interest-cohort=()"],"X-Frame-Options":["DENY"],"X-Content-Type-Options":["nosniff"],"X-Robots-Tag":["noindex, nofollow"],"Content-Encoding":["deflate"],"Vary":["Accept-Encoding"],"Content-Length":["1572"],"Referrer-Policy":["no-referrer-when-downgrade"],"Strict-Transport-Security":["max-age=31536000"]}}

And here is the same request with curl -v:

# curl -v -X POST -d "Username=someone&Pw=somepwd" "https://somedomain/emby/Users/authenticatebyname?X-Emby-Client=Emby%20for%20Android&X-Emby-Device-Name=POCO%20X3%20NFC&X-Emby-Device-Id=someid&X-Emby-Client-Version=3.3.07&X-Emby-Language=es"
Note: Unnecessary use of -X or --request, POST is already inferred.
* processing: https://somedomain/emby/Users/authenticatebyname?X-Emby-Client=Emby%20for%20Android&X-Emby-Device-Name=POCO%20X3%20NFC&X-Emby-Device-Id=someid&X-Emby-Client-Version=3.3.07&X-Emby-Language=es
*   Trying 127.0.0.1:443...
* Connected to somedomain (127.0.0.1) port 443
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=somedomain
*  start date: Jul 27 08:10:41 2023 GMT
*  expire date: Oct 25 08:10:40 2023 GMT
*  subjectAltName: host "somedomain" matched cert's "somedomain"
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* using HTTP/2
* h2 [:method: POST]
* h2 [:scheme: https]
* h2 [:authority: somedomain]
* h2 [:path: /emby/Users/authenticatebyname?X-Emby-Client=Emby%20for%20Android&X-Emby-Device-Name=POCO%20X3%20NFC&X-Emby-Device-Id=41ccce2f6dca9e0f&X-Emby-Client-Version=3.3.07&X-Emby-Language=es]
* h2 [user-agent: curl/8.2.0]
* h2 [accept: */*]
* h2 [content-length: 33]
* h2 [content-type: application/x-www-form-urlencoded]
* Using Stream ID: 1
> POST /emby/Users/authenticatebyname?X-Emby-Client=Emby%20for%20Android&X-Emby-Device-Name=POCO%20X3%20NFC&X-Emby-Device-Id=41ccce2f6dca9e0f&X-Emby-Client-Version=3.3.07&X-Emby-Language=es HTTP/2
> Host: somedomain
> User-Agent: curl/8.2.0
> Accept: */*
> Content-Length: 33
> Content-Type: application/x-www-form-urlencoded
>
< HTTP/2 200
< access-control-allow-headers: Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Token, X-Emby-Client, X-Emby-Client-Version, X-Emby-Device-Id, X-Emby-Device-Name, X-Emby-Authorization
< access-control-allow-methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
< access-control-allow-origin: *
< access-control-allow-private-network: true
< alt-svc: h3=":443"; ma=2592000
< content-type: application/json; charset=utf-8
< date: Fri, 28 Jul 2023 02:23:31 GMT
< expires: -1
< permissions-policy: interest-cohort=()
< private-network-access-id: 55086a1b7d174fad9a34d78c4d3322a3
< private-network-access-name: somedomain
< referrer-policy: no-referrer-when-downgrade
< server: Caddy
< strict-transport-security: max-age=31536000
< vary: Accept-Encoding
< x-content-type-options: nosniff
< x-frame-options: DENY
< x-robots-tag: noindex, nofollow
< x-xss-protection: 1; mode=block
<
{"User":{"Name":"someone","ServerId":"someid","Prefix":"C","DateCreated":"0001-01-01T00:00:00.0000000Z","Id":"someid","HasPassword":true,"HasConfiguredPassword":true,"HasConfiguredEasyPassword":false,"LastLoginDate":"2023-07-28T02:23:32.0986640Z","LastActivityDate":"2023-07-28T02:23:32.1041271Z","Configuration":{"AudioLanguagePreference":"","PlayDefaultAudioTrack":true,"SubtitleLanguagePreference":"pol","DisplayMissingEpisodes":false,"SubtitleMode":"Default","EnableLocalPassword":false,"OrderedViews":["someid","someid","someid","someid","someid","someid"],"LatestItemsExcludes":[],"MyMediaExcludes":[],"HidePlayedInLatest":true,"RememberAudioSelections":true,"RememberSubtitleSelections":true,"EnableNextEpisodeAutoPlay":false,"ResumeRewindSeconds":0,"IntroSkipMode":"ShowButton"},"Policy":{"IsAdministrator":false,"IsHidden":true,"IsHiddenRemotely":true,"IsHiddenFromUnusedDevices":false,"IsDisabled":false,"BlockedTags":[],"IsTagBlockingModeInclusive":false,"IncludeTags":[],"EnableUserPreferenceAccess":true,"AccessSchedules":[],"BlockUnratedItems":[],"EnableRemoteControlOfOtherUsers":false,"EnableSharedDeviceControl":false,"EnableRemoteAccess":true,"EnableLiveTvManagement":false,"EnableLiveTvAccess":true,"EnableMediaPlayback":true,"EnableAudioPlaybackTranscoding":true,"EnableVideoPlaybackTranscoding":false,"EnablePlaybackRemuxing":true,"EnableContentDeletion":false,"EnableContentDeletionFromFolders":[],"EnableContentDownloading":true,"EnableSubtitleDownloading":false,"EnableSubtitleManagement":false,"EnableSyncTranscoding":false,"EnableMediaConversion":false,"EnabledChannels":[],"EnableAllChannels":true,"EnabledFolders":[],"EnableAllFolders":true,"InvalidLoginAttemptCount":0,"EnablePublicSharing":false,"RemoteClientBitrateLimit":0,"AuthenticationProviderId":"Emby.Server.Implementations.Library.DefaultAuthenticationProvider","ExcludedSubFolders":[],"SimultaneousStreamLimit":4,"EnabledDevices":[],"EnableAllDevices":true}},"SessionInfo":{"PlayState":{"CanSeek":false,"IsPaused":false,"IsMuted":false,"RepeatMode":"RepeatNone","SubtitleOffset":0,"PlaybackRate":1},"AdditionalUsers":[],"RemoteEndPoint":"127.0.0.1","Protocol":"HTTP/1.1","PlayableMediaTypes":["Audio","Video"],"PlaylistIndex":0,"PlaylistLength":0,"Id":"someid","ServerId":"someid","UserId":"someid","UserName":"someone","Client":"Emby for Android","LastActivityDate":"2023-07-28T02:23:32.1041271Z","DeviceName":"POCO X3 NFC","InternalDeviceId":someid,"DeviceId":"someid","ApplicationVersion":"3.3.07","AppIconUrl":"https://github.com/MediaBrowser/Emby.Resources/raw/master/images/devices/android.png","SupportedCommands":["MoveUp","MoveDown","MoveLeft","MoveRight","PageUp","PageDown","PreviousLetter","NextLetter","ToggleOsd","ToggleContextMenu","Select","Back","SendKey","SendS* Connection #0 to host somedomain left intact
tring","GoHome","GoToSettings","VolumeUp","VolumeDown","Mute","Unmute","ToggleMute","SetVolume","SetAudioStreamIndex","SetSubtitleStreamIndex","RefreshMediaSource","DisplayContent","GoToSearch","DisplayMessage","SetRepeatMode","SetSubtitleOffset","SetPlaybackRate","ChannelUp","ChannelDown","PlayMediaSource","PlayTrailers"],"SupportsRemoteControl":true},"AccessToken":"someid","ServerId":"someid"}

As for your question to try without the % symbols, when I try the folowing I get a syntax error in Caddyfile:

query X-Emby-Client=Emby for Android
query X-Emby-Client="Emby for Android"

The syntax error is in both cases:

Error during parsing: malformed query matcher token: Android; must be in param=val format

But that’s probably because i’m a noob and don’t know how to write the unencoded way.

You need to use quotes around the entire token, which includes the left side of the =.

1 Like

That was it! Thanks a lot for the help!

1 Like

Ahhh interesting. I wonder if we should require URL-encoded values there like what appears in the actual URL… then again, that’s a pain and hard to read (and we’re not parsing a URL here, just setting a value).

Maybe it’s enough to include this kind of example (whitespaces query param) in the documentation. Because I read like 30 times this: Request matchers (Caddyfile) — Caddy Documentation

But no clue about to use unencoded way and quotes around the entire token when it has white spaces.

For anyone reading the solution was this:

query "<key>=<val>"

In my particular case:

query "X-Emby-Client=Emby for Android"
2 Likes

Tokens are explained here: Caddyfile Concepts — Caddy Documentation this page is essential reading for writing a Caddyfile.

I think we should document that query matcher values match the un-encoded form. We could also try matching the encoded form if we see % in the input string :thinking:

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.