Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLeitwolf112026-11-09 00:38:47 +0300
committerLeitwolf112026-11-09 00:38:47 +0300
commit66f3cc196a9b91d471a90e021b12630100181c4a (patch)
treea3cb0422c44094f1d00ddb312e9035afe1096c86
parentd28c8c216708c3bb59773722e6235c4c8cbb2b73 (diff)
downloadprogmaweb-66f3cc196a9b91d471a90e021b12630100181c4a.tar.gz
Rate-limit sign-in by visitor behind Cloudflare and stop forwarding h2c
Cloudflare proxies the Progmasoft hosts, so Nginx saw a Cloudflare edge as its peer and appended that edge to X-Forwarded-For. The API takes the last hop as the client, so the account-auth limit (12 attempts in 3 minutes) was counted per edge: 38 failed sign-ins from one address in a row were all answered with 401, none with 429, and unrelated visitors behind one edge shared a bucket. - ops/nginx/cloudflare-real-ip.conf trusts CF-Connecting-IP only from the published Cloudflare ranges; a direct peer keeps its own address, so the header cannot be forged around Cloudflare. With it installed the 13th failed sign-in gets 429, a forged X-Forwarded-For changes nothing, and another address is not affected. - Every proxied location forwarded the client's Upgrade header with "Connection: upgrade" on each request. Only a WebSocket upgrade is now forwarded; other requests carry no Upgrade and an empty Connection. - c8 reached glob 13 (GHSA-3g3m-2fjr-6pw4) and minimatch 10 (GHSA-g3hv-75wg-m249) through test-exclude; both are overridden to the fixed majors. pnpm audit reports nothing, coverage, lint and check pass. - The CodeQL action pins carry their exact version comment (v4.38.2). - ops/README.md describes the address handling and no longer names a script that left the repository with AI Support. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--.github/workflows/codeql.yml4
-rw-r--r--ops/README.md14
-rw-r--r--ops/nginx/cloudflare-real-ip.conf37
-rw-r--r--ops/nginx/progmaweb.conf37
-rw-r--r--pnpm-lock.yaml38
-rw-r--r--pnpm-workspace.yaml8
6 files changed, 106 insertions, 32 deletions
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 1c6d4bd..6d11636 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -35,7 +35,7 @@ jobs:
if: matrix.language == 'csharp'
with:
dotnet-version: 10.0.x
- - uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4
+ - uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build_mode }}
@@ -46,7 +46,7 @@ jobs:
run: |
dotnet restore Progmaweb.slnx --locked-mode
dotnet build Progmaweb.slnx --configuration Release --no-restore
- - uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4
+ - uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
category: /language:${{ matrix.language }}
diff --git a/ops/README.md b/ops/README.md
index 283ca0b..554f085 100644
--- a/ops/README.md
+++ b/ops/README.md
@@ -104,9 +104,17 @@ could be registered by someone else. Delete the dump once the deletion is confir
The Privacy Policy says that IP addresses are kept only with ViGet uploads. On both servers the `main` log format of
`/etc/nginx/nginx.conf` therefore has no `$remote_addr` and no `$http_x_forwarded_for`, and `error_log` keeps only
-`crit` entries, because a lower level names the client of a failed request. The change is
-`ops/aisupport/nginx-no-ip.pl` (applied by hand on the first server on 2026-11-06, with a copy of the previous file
-in `/root/policy-20261106`). A new site in `conf.d` uses the `main` format or one of its own without an address.
+`crit` entries, because a lower level names the client of a failed request. The change was applied by hand on the
+first server on 2026-11-06, with a copy of the previous file in `/root/policy-20261106`. A new site in `conf.d` uses the `main` format or one of its own without an address.
+
+### Visitor addresses behind Cloudflare
+
+Cloudflare proxies the hosts, so Nginx is reached by Cloudflare edges, and one visitor's requests arrive from
+many of them. `nginx/cloudflare-real-ip.conf`, installed as `/etc/nginx/conf.d/00-cloudflare-real-ip.conf`, takes
+the visitor from `CF-Connecting-IP` when, and only when, the peer is in a Cloudflare range. Without it the API
+rate-limits sign-in by edge: one visitor never reaches the limit and unrelated visitors share one. Compare its ranges
+with <https://www.cloudflare.com/ips/> when Cloudflare announces a change. The address is used for the limit
+alone; the logs above still keep none.
### Releases and rollback
diff --git a/ops/nginx/cloudflare-real-ip.conf b/ops/nginx/cloudflare-real-ip.conf
new file mode 100644
index 0000000..2162842
--- /dev/null
+++ b/ops/nginx/cloudflare-real-ip.conf
@@ -0,0 +1,37 @@
+# SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1
+
+# Installed as /etc/nginx/conf.d/00-cloudflare-real-ip.conf on the first server.
+#
+# Cloudflare proxies the Progmasoft, Tala and Tazel hosts, so the peer of
+# Nginx is a Cloudflare edge, and each request may come from another edge.
+# Without this file the address the API rate-limits by is that edge: the
+# sign-in limit then never fills for one visitor and is shared by unrelated
+# ones. A connection from a Cloudflare range takes the visitor from
+# CF-Connecting-IP; any other peer keeps its own address, so the header cannot
+# be forged by a direct connection to the server. The ranges are those of
+# https://www.cloudflare.com/ips/ (checked 2026-11-09).
+
+set_real_ip_from 173.245.48.0/20;
+set_real_ip_from 103.21.244.0/22;
+set_real_ip_from 103.22.200.0/22;
+set_real_ip_from 103.31.4.0/22;
+set_real_ip_from 141.101.64.0/18;
+set_real_ip_from 108.162.192.0/18;
+set_real_ip_from 190.93.240.0/20;
+set_real_ip_from 188.114.96.0/20;
+set_real_ip_from 197.234.240.0/22;
+set_real_ip_from 198.41.128.0/17;
+set_real_ip_from 162.158.0.0/15;
+set_real_ip_from 104.16.0.0/13;
+set_real_ip_from 104.24.0.0/14;
+set_real_ip_from 172.64.0.0/13;
+set_real_ip_from 131.0.72.0/22;
+set_real_ip_from 2400:cb00::/32;
+set_real_ip_from 2606:4700::/32;
+set_real_ip_from 2803:f800::/32;
+set_real_ip_from 2405:b500::/32;
+set_real_ip_from 2405:8100::/32;
+set_real_ip_from 2a06:98c0::/29;
+set_real_ip_from 2c0f:f248::/32;
+real_ip_header CF-Connecting-IP;
diff --git a/ops/nginx/progmaweb.conf b/ops/nginx/progmaweb.conf
index 3cbb21a..daa81ef 100644
--- a/ops/nginx/progmaweb.conf
+++ b/ops/nginx/progmaweb.conf
@@ -1,6 +1,19 @@
# SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
# SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1
+# Only a WebSocket upgrade reaches the applications. Forwarding any Upgrade
+# header with "Connection: upgrade" on every request would let a client ask an
+# upstream for another protocol (h2c) behind Nginx.
+map $http_upgrade $progmaweb_upgrade {
+ default "";
+ ~*^websocket$ websocket;
+}
+
+map $progmaweb_upgrade $progmaweb_connection {
+ default "";
+ websocket upgrade;
+}
+
server {
listen 80;
listen [::]:80;
@@ -87,8 +100,8 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
+ proxy_set_header Upgrade $progmaweb_upgrade;
+ proxy_set_header Connection $progmaweb_connection;
proxy_read_timeout 60s;
}
}
@@ -122,8 +135,8 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
+ proxy_set_header Upgrade $progmaweb_upgrade;
+ proxy_set_header Connection $progmaweb_connection;
proxy_read_timeout 60s;
}
}
@@ -206,8 +219,8 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
+ proxy_set_header Upgrade $progmaweb_upgrade;
+ proxy_set_header Connection $progmaweb_connection;
proxy_read_timeout 60s;
}
}
@@ -271,8 +284,8 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
+ proxy_set_header Upgrade $progmaweb_upgrade;
+ proxy_set_header Connection $progmaweb_connection;
proxy_read_timeout 60s;
}
}
@@ -337,8 +350,8 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
+ proxy_set_header Upgrade $progmaweb_upgrade;
+ proxy_set_header Connection $progmaweb_connection;
proxy_read_timeout 60s;
}
}
@@ -399,8 +412,8 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
+ proxy_set_header Upgrade $progmaweb_upgrade;
+ proxy_set_header Connection $progmaweb_connection;
proxy_read_timeout 60s;
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6d6f591..42ca720 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7,6 +7,8 @@ settings:
overrides:
brace-expansion@>=5.0.0 <5.0.12: 5.0.12
zod@<4.6.6: 4.6.7
+ glob@>=9.0.0 <14.0.2: 14.0.4
+ minimatch@>=7.0.0 <11.0.1: 11.0.5
importers:
@@ -360,6 +362,9 @@ packages:
cpu: [x64]
os: [win32]
+ '@isaacs/nonrecursive-glob-match@1.1.0':
+ resolution: {integrity: sha512-GPm/Dbvk5RLmhRnxXIa7nTpK0xtl4E0wj0MslsuNiJ1FIROU+2+9lgUC/dgF7Ey4QCWhfEsgxilMRMCdHPfDfw==}
+
'@istanbuljs/schema@0.1.6':
resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==}
engines: {node: '>=8'}
@@ -1173,8 +1178,8 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
- glob@13.0.6:
- resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
+ glob@14.0.4:
+ resolution: {integrity: sha512-mMiwNKoJZ4HAIRVJxotO6LsNRReA30gQwP9E2/p94tfLv+jygCNpKDjhhSL6OLY6LE371EZm5EAf7RpYuUqlsw==}
engines: {node: 18 || 20 || >=22}
globals@14.0.0:
@@ -1460,9 +1465,9 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
- minimatch@10.2.6:
- resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
- engines: {node: 18 || 20 || >=22}
+ minimatch@11.0.5:
+ resolution: {integrity: sha512-snPlVppFcYWfLLFQGhDxUyBcHNtMlWmn3jtj7pEaO+DP8pqL0IvAzXhMYmn1z9ASNtgmwLBdFxSf4tMUB0OTKA==}
+ engines: {node: 20 || >=22}
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
@@ -1582,8 +1587,8 @@ packages:
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
- path-scurry@2.0.2:
- resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
+ path-scurry@3.0.0:
+ resolution: {integrity: sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==}
engines: {node: 18 || 20 || >=22}
picocolors@1.1.1:
@@ -2250,6 +2255,8 @@ snapshots:
'@img/sharp-win32-x64@0.35.5':
optional: true
+ '@isaacs/nonrecursive-glob-match@1.1.0': {}
+
'@istanbuljs/schema@0.1.6': {}
'@jridgewell/gen-mapping@0.3.13':
@@ -2423,7 +2430,7 @@ snapshots:
'@typescript-eslint/types': 8.68.0
'@typescript-eslint/visitor-keys': 8.68.0
debug: 4.4.3(supports-color@7.2.0)
- minimatch: 10.2.6
+ minimatch: 11.0.5
semver: 7.8.5
tinyglobby: 0.2.17
ts-api-utils: 2.5.0(typescript@6.0.3)
@@ -3208,11 +3215,11 @@ snapshots:
dependencies:
is-glob: 4.0.3
- glob@13.0.6:
+ glob@14.0.4:
dependencies:
- minimatch: 10.2.6
+ minimatch: 11.0.5
minipass: 7.1.3
- path-scurry: 2.0.2
+ path-scurry: 3.0.0
globals@14.0.0: {}
@@ -3485,8 +3492,9 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.2
- minimatch@10.2.6:
+ minimatch@11.0.5:
dependencies:
+ '@isaacs/nonrecursive-glob-match': 1.1.0
brace-expansion: 5.0.12
minimatch@3.1.5:
@@ -3615,7 +3623,7 @@ snapshots:
path-parse@1.0.7: {}
- path-scurry@2.0.2:
+ path-scurry@3.0.0:
dependencies:
lru-cache: 11.5.3
minipass: 7.1.3
@@ -3908,8 +3916,8 @@ snapshots:
test-exclude@8.0.0:
dependencies:
'@istanbuljs/schema': 0.1.6
- glob: 13.0.6
- minimatch: 10.2.6
+ glob: 14.0.4
+ minimatch: 11.0.5
tinyglobby@0.2.17:
dependencies:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index ac1f58f..74a255c 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -16,6 +16,14 @@ overrides:
# validating oversized constrained collections (fixed in 4.6.6). The plugin
# accepts any zod 4; remove this once it resolves a fixed release itself.
"zod@<4.6.6": 4.6.7
+ # c8 reaches glob 13 and minimatch 10 through test-exclude 8, the latest
+ # release. glob before 14.0.2 keeps walking the file system after its
+ # AbortSignal fired, and minimatch before 11.0.1 can be made to expand a
+ # pattern without bound. test-exclude uses only their stable glob and match
+ # calls, which the fixed majors keep. Remove these once test-exclude
+ # depends on fixed releases itself.
+ "glob@>=9.0.0 <14.0.2": 14.0.4
+ "minimatch@>=7.0.0 <11.0.1": 11.0.5
auditConfig:
ignoreGhsas:
# braces <= 3.0.3: stack exhaustion on deeply nested brace patterns. No