83 lines
2.9 KiB
Bash
83 lines
2.9 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# fuzz.sh — énumère les endpoints d'une API et cartographie les codes
|
||
|
|
# de réponse, pour repérer les routes ouvertes, les fuites d'info dans
|
||
|
|
# les messages d'erreur, et les variations de comportement.
|
||
|
|
#
|
||
|
|
# USAGE
|
||
|
|
# ./fuzz.sh [targets.env] [endpoints.txt]
|
||
|
|
#
|
||
|
|
# Lit une liste de chemins (un par ligne) depuis le 2e argument, sinon
|
||
|
|
# FUZZ_PATHS, sinon ./endpoints.txt à côté du script. Pour chaque
|
||
|
|
# chemin, émet GET et OPTIONS (pour détecter les verbes autorisés via
|
||
|
|
# l'en-tête Allow).
|
||
|
|
#
|
||
|
|
# Journalise dans $OUTPUT_DIR/api-fuzz.log.
|
||
|
|
|
||
|
|
COMMON_SH="$(cd "$(dirname "$0")/../.." && pwd)/lib/common.sh"
|
||
|
|
# shellcheck disable=SC1091
|
||
|
|
source "$COMMON_SH"
|
||
|
|
load_target "${1:-}"
|
||
|
|
|
||
|
|
LIST="${2:-${FUZZ_PATHS:-$(dirname "$0")/endpoints.txt}}"
|
||
|
|
[[ -f "$LIST" ]] \
|
||
|
|
|| die "Liste d'endpoints introuvable : $LIST (créez endpoints.txt ou réglez FUZZ_PATHS)"
|
||
|
|
|
||
|
|
mkdir -p "${OUTPUT_DIR:-./output}"
|
||
|
|
LOG="${OUTPUT_DIR:-./output}/api-fuzz.log"
|
||
|
|
: > "$LOG"
|
||
|
|
|
||
|
|
info "Fuzz de $(wc -l < "$LIST") endpoints sur ${#AUTHORIZED_HOSTS[@]} hôte(s) du périmètre"
|
||
|
|
info "Liste : $LIST"
|
||
|
|
info "Journal : $LOG"
|
||
|
|
|
||
|
|
# Endpoints **intentionnellement publics** : un 200 dessus est normal,
|
||
|
|
# pas une anomalie. Ex : la discovery OIDC d'IdentityServer, que les
|
||
|
|
# clients (PostIt) interrogent pour découvrir les endpoints d'auth.
|
||
|
|
EXPECTED_PUBLIC=(
|
||
|
|
"/.well-known/openid-configuration"
|
||
|
|
"/.well-known/jwks"
|
||
|
|
)
|
||
|
|
is_expected_public() {
|
||
|
|
local p="$1"
|
||
|
|
local e
|
||
|
|
for e in "${EXPECTED_PUBLIC[@]}"; do [[ "$e" == "$p" ]] && return 0; done
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
|
||
|
|
while IFS= read -r base; do
|
||
|
|
info "→ $base"
|
||
|
|
while IFS= read -r p || [[ -n "$p" ]]; do
|
||
|
|
[[ -z "$p" || "$p" == \#* ]] && continue
|
||
|
|
url="${base}${p}"
|
||
|
|
http_probe GET "$url"
|
||
|
|
code="$LAST_CODE"
|
||
|
|
size=0
|
||
|
|
[[ -n "$LAST_BODY" && -s "$LAST_BODY" ]] && size=$(wc -c < "$LAST_BODY")
|
||
|
|
printf '%s\tGET\t%s\t%s\t%8d\t%s\n' "$(date -Is)" "$base" "$code" "$size" "$p" >> "$LOG"
|
||
|
|
|
||
|
|
# Verbes autorisés : la réponse à OPTIONS peut révéler POST/DELETE
|
||
|
|
# même sur une route GET-only côté serveur.
|
||
|
|
allow="$(set +e; curl -s -D - -o /dev/null -X OPTIONS \
|
||
|
|
--connect-timeout "${CURL_TIMEOUT:-10}" --max-time "${CURL_MAX_TIME:-30}" \
|
||
|
|
"$url" | tr -d '\r' | awk -F': ' 'tolower($1)=="allow"{print $2}')"
|
||
|
|
[[ -n "$allow" ]] && printf '%s\tOPT\t%s\tAllow: %s\t%s\n' "$(date -Is)" "$base" "$allow" "$p" >> "$LOG"
|
||
|
|
|
||
|
|
case "$code" in
|
||
|
|
200)
|
||
|
|
if is_expected_public "$p"; then
|
||
|
|
ok "200 $base$p — public attendu (OIDC discovery)"
|
||
|
|
else
|
||
|
|
warn "200 $base$p — accessible"
|
||
|
|
fi
|
||
|
|
;;
|
||
|
|
401|403) ;;
|
||
|
|
404) ;;
|
||
|
|
500) err "500 $base$p — erreur serveur (fuite d'info possible)" ;;
|
||
|
|
*) warn "$code $base$p" ;;
|
||
|
|
esac
|
||
|
|
drop_body
|
||
|
|
pace
|
||
|
|
done < "$LIST"
|
||
|
|
done < <(each_scope_url)
|
||
|
|
|
||
|
|
info "Terminé. Voir $LOG."
|