283 lines
11 KiB
Plaintext
283 lines
11 KiB
Plaintext
# =============================================================
|
|
# OxiCloud — Baseline: NC Basic Auth failure modes
|
|
# =============================================================
|
|
# Group P from BASELINE_TESTS_NC_WEBDAV.md.
|
|
#
|
|
# Coverage:
|
|
# P1 — no Authorization header → 401 + WWW-Authenticate
|
|
# P2 — wrong password (real user) → 401
|
|
# P3 — N wrong attempts from same IP against a THROWAWAY
|
|
# username trip the per-(account,IP) lockout
|
|
# P4 — per-IP lockout scope (the #323 regression guard):
|
|
# 6 wrong attempts from spoofed X-Forwarded-For: IP1
|
|
# lock (admin, IP1), but the SAME correct credential
|
|
# from spoofed X-Forwarded-For: IP2 still succeeds.
|
|
# Depends on OXICLOUD_TRUST_PROXY_CIDR=0.0.0.0/0 in
|
|
# tests/common/server.env so the server honours the
|
|
# X-Forwarded-For header on localhost.
|
|
#
|
|
# Deliberately NOT covered here:
|
|
# P5 — External user attempts NC Basic Auth. Externals can't
|
|
# mint app passwords in the first place (the upstream gate
|
|
# is asserted in tests/api/external_users.hurl), so the
|
|
# in-middleware belt-and-braces check is unreachable via
|
|
# a black-box HTTP test. Verified by code inspection.
|
|
# =============================================================
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Setup 1 — JWT login (needed to mint the app password for P4's
|
|
# positive control).
|
|
# ─────────────────────────────────────────────────────────────
|
|
POST {{base_url}}/api/auth/login
|
|
Content-Type: application/json
|
|
{ "username": "{{username}}", "password": "{{password}}" }
|
|
|
|
HTTP 200
|
|
[Captures]
|
|
jwt: jsonpath "$.access_token"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Setup 2 — Mint admin's NC app password.
|
|
# ─────────────────────────────────────────────────────────────
|
|
POST {{base_url}}/api/auth/app-passwords
|
|
Authorization: Bearer {{jwt}}
|
|
Content-Type: application/json
|
|
{ "label": "nc_auth_failures P4 positive control" }
|
|
|
|
HTTP 200
|
|
[Captures]
|
|
nc_username: jsonpath "$.username"
|
|
nc_password: jsonpath "$.password"
|
|
ap_id: jsonpath "$.id"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# P1 — PROPFIND without any Authorization header
|
|
# → 401 with `WWW-Authenticate: Basic realm="OxiCloud"`.
|
|
# NC desktop relies on this challenge to know it should
|
|
# offer credentials at all.
|
|
# ─────────────────────────────────────────────────────────────
|
|
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
|
|
|
HTTP 401
|
|
[Asserts]
|
|
header "WWW-Authenticate" contains "Basic"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# P2 — Wrong password against a real user
|
|
# → 401 with the same WWW-Authenticate challenge.
|
|
# Anti-enumeration: response shape identical whether the
|
|
# user exists or not.
|
|
# ─────────────────────────────────────────────────────────────
|
|
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
|
[BasicAuth]
|
|
{{username}}: definitely-wrong-password
|
|
|
|
HTTP 401
|
|
[Asserts]
|
|
header "WWW-Authenticate" contains "Basic"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# P3 — Lockout trip
|
|
#
|
|
# Consecutive bad-credential attempts against a throwaway
|
|
# username trip the per-(account, IP) lockout. The default
|
|
# threshold is 5 (`OXICLOUD_LOCKOUT_MAX_FAILURES = 5`) and
|
|
# `tests/common/server.env` does NOT override it — the only
|
|
# raised values in that env are the broader login / refresh /
|
|
# register rate-limits, which prevent the rate-limiter from
|
|
# firing AHEAD of the lockout under sustained test traffic.
|
|
# The lockout itself still engages on the 6th attempt.
|
|
#
|
|
# Once engaged, every subsequent attempt for the same
|
|
# (account, IP) pair short-circuits to 401 from the lockout
|
|
# check, BEFORE app-password verification runs. The audit log
|
|
# fires `WARN account_temporarily_locked` from
|
|
# `login_lockout_service.rs` when the cap is hit — useful
|
|
# correlation signal during a real run.
|
|
#
|
|
# A throwaway username (`nc-lockout-probe-…`) is used because
|
|
# the lockout is keyed by (username, IP); locking a throwaway
|
|
# pair never poisons admin's auth path, so downstream Hurl
|
|
# tests in run.sh that authenticate as admin keep working.
|
|
#
|
|
# Limitation of a black-box HTTP probe: the wire response is
|
|
# 401 with the same WWW-Authenticate header whether the 401
|
|
# comes from "lockout engaged" or "still just rejecting bad
|
|
# creds" — both look the same on the wire. Verification of
|
|
# the lockout-engaged branch specifically lives in the unit
|
|
# tests (`login_lockout_service.rs::tests`). What this Hurl
|
|
# test guards is that 7 consecutive attempts keep returning
|
|
# the same 401 shape (no 500s, no header drift), and the
|
|
# server-side audit log confirms the lockout engaged at 5.
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
|
|
[BasicAuth]
|
|
nc-lockout-probe-001: bad-1
|
|
|
|
HTTP 401
|
|
|
|
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
|
|
[BasicAuth]
|
|
nc-lockout-probe-001: bad-2
|
|
|
|
HTTP 401
|
|
|
|
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
|
|
[BasicAuth]
|
|
nc-lockout-probe-001: bad-3
|
|
|
|
HTTP 401
|
|
|
|
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
|
|
[BasicAuth]
|
|
nc-lockout-probe-001: bad-4
|
|
|
|
HTTP 401
|
|
|
|
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
|
|
[BasicAuth]
|
|
nc-lockout-probe-001: bad-5
|
|
|
|
HTTP 401
|
|
|
|
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
|
|
[BasicAuth]
|
|
nc-lockout-probe-001: bad-6
|
|
|
|
HTTP 401
|
|
|
|
# 7th attempt: account+IP is now locked at the middleware level.
|
|
# Continues to return 401.
|
|
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
|
|
[BasicAuth]
|
|
nc-lockout-probe-001: bad-7
|
|
|
|
HTTP 401
|
|
[Asserts]
|
|
header "WWW-Authenticate" contains "Basic"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# P4 — Per-IP lockout scope (#323 regression guard)
|
|
#
|
|
# Goal: prove that locking out (admin, IP1) does NOT lock out
|
|
# (admin, IP2) — the lockout is keyed by *both* parts, not by
|
|
# username alone. This was the gap the reporter demonstrated:
|
|
# an attacker spoofing X-Forwarded-For could lock a legitimate
|
|
# user out from their own IP. The fix scoped the key.
|
|
#
|
|
# Mechanic:
|
|
# 1. Pre-check: admin's app password works from the test's
|
|
# default client IP (127.0.0.1, no X-Forwarded-For).
|
|
# 2. 6 wrong attempts with X-Forwarded-For: 10.0.0.1 trip the
|
|
# lockout for (admin, 10.0.0.1).
|
|
# 3. CORRECT app password with X-Forwarded-For: 10.0.0.1 →
|
|
# 401 (still locked from THIS IP — positive demonstration
|
|
# that the lockout actually engaged, not just chance).
|
|
# 4. CORRECT app password with X-Forwarded-For: 10.0.0.2 →
|
|
# 207 (NOT locked from this IP — the load-bearing
|
|
# assertion of P4).
|
|
#
|
|
# Why this doesn't break the rest of the suite: every other
|
|
# test runs from the default client IP (127.0.0.1) without
|
|
# X-Forwarded-For, so (admin, 127.0.0.1) is untouched. The
|
|
# lockouts placed here are on (admin, 10.0.0.1) and
|
|
# (admin, 10.0.0.2 — released by the success), neither of
|
|
# which any other test touches.
|
|
#
|
|
# Requires: OXICLOUD_TRUST_PROXY_CIDR=0.0.0.0/0 in
|
|
# tests/common/server.env.
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
# ── Pre-check: app password works with no X-Forwarded-For ───
|
|
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
|
[BasicAuth]
|
|
{{nc_username}}: {{nc_password}}
|
|
|
|
HTTP 207
|
|
|
|
|
|
# ── Step 1: burn the lockout for (admin, 10.0.0.1) ──────────
|
|
|
|
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
|
X-Forwarded-For: 10.0.0.1
|
|
[BasicAuth]
|
|
{{username}}: p4-bad-1
|
|
|
|
HTTP 401
|
|
|
|
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
|
X-Forwarded-For: 10.0.0.1
|
|
[BasicAuth]
|
|
{{username}}: p4-bad-2
|
|
|
|
HTTP 401
|
|
|
|
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
|
X-Forwarded-For: 10.0.0.1
|
|
[BasicAuth]
|
|
{{username}}: p4-bad-3
|
|
|
|
HTTP 401
|
|
|
|
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
|
X-Forwarded-For: 10.0.0.1
|
|
[BasicAuth]
|
|
{{username}}: p4-bad-4
|
|
|
|
HTTP 401
|
|
|
|
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
|
X-Forwarded-For: 10.0.0.1
|
|
[BasicAuth]
|
|
{{username}}: p4-bad-5
|
|
|
|
HTTP 401
|
|
|
|
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
|
X-Forwarded-For: 10.0.0.1
|
|
[BasicAuth]
|
|
{{username}}: p4-bad-6
|
|
|
|
HTTP 401
|
|
|
|
|
|
# ── Step 2: CORRECT app password from IP1 — still 401 ──────
|
|
# This is the positive demonstration that the lockout engaged.
|
|
# If this were 207, the lockout would not have fired and the
|
|
# subsequent IP2 success wouldn't prove anything.
|
|
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
|
X-Forwarded-For: 10.0.0.1
|
|
[BasicAuth]
|
|
{{nc_username}}: {{nc_password}}
|
|
|
|
HTTP 401
|
|
|
|
|
|
# ── Step 3: CORRECT app password from IP2 — 207 ────────────
|
|
# The load-bearing assertion of P4: a successful auth from a
|
|
# DIFFERENT spoofed source IP proves the lockout was scoped
|
|
# to (admin, 10.0.0.1) and not to admin alone.
|
|
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
|
X-Forwarded-For: 10.0.0.2
|
|
[BasicAuth]
|
|
{{nc_username}}: {{nc_password}}
|
|
|
|
HTTP 207
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Teardown — Revoke the app password we minted. Keeps the
|
|
# app-passwords table clean across re-runs.
|
|
# ─────────────────────────────────────────────────────────────
|
|
DELETE {{base_url}}/api/auth/app-passwords/{{ap_id}}
|
|
Authorization: Bearer {{jwt}}
|
|
|
|
HTTP 200
|