Authorization #

Authorization answers a different question from authentication: not who you are, but what you’re allowed to do. After the system verifies a user’s identity, it must decide whether that identity has the right to perform the requested operation — reading certain data, modifying a resource, accessing admin pages, or deleting an entry.

Many systems have strong authentication but weak authorization. They ensure only logged-in users can access APIs — but forget to verify whether the logged-in user is authorized to access the specific resource being requested. The result is a hole called Insecure Direct Object Reference (IDOR): user A can access user B’s data just by changing a number in the URL.

Correct authorization isn’t just about high-level roles and permissions. It’s about ensuring every operation, on every resource, is validated against the identity performing it — at the most granular level.

Authorization Models: Choosing the Right One #

There are several common authorization models, each fitting different contexts. Choosing the right one depends heavily on business complexity.

Role-Based Access Control (RBAC) #

RBAC is the most common model: users are assigned one or more roles, and roles determine the permissions held. Easy to understand, easy to implement, suitable for most applications.

RBAC structure:

flowchart LR
    User["User"] -->|"has"| Role["Role(s)"]
    Role -->|"has"| Permission["Permission(s)"]

Example: User Ali → Role: “editor” Role “editor” → Permissions: [“articles:create”, “articles:edit_own”, “articles:publish”, “comments:moderate”]

User Budi → Role: “viewer” Role “viewer” → Permissions: [“articles:read”, “comments:read”]

User Admin → Role: “admin” Role “admin” → Permissions: ["*"] (all permissions)

// A clean, extensible RBAC implementation
type Permission string

const (
    ArticlesRead      Permission = "articles:read"
    ArticlesCreate    Permission = "articles:create"
    ArticlesEditOwn   Permission = "articles:edit_own"
    ArticlesEditAny   Permission = "articles:edit_any"
    ArticlesDeleteOwn Permission = "articles:delete_own"
    ArticlesDeleteAny Permission = "articles:delete_any"
    ArticlesPublish   Permission = "articles:publish"

    UsersRead   Permission = "users:read"
    UsersManage Permission = "users:manage"

    AdminAccess Permission = "admin:access"
)

// Role definitions — single source of truth
var rolePermissions = map[string]map[Permission]bool{
    "viewer": {
        ArticlesRead: true,
        UsersRead:    true,
    },
    "author": {
        ArticlesRead:      true,
        ArticlesCreate:    true,
        ArticlesEditOwn:   true,
        ArticlesDeleteOwn: true,
        UsersRead:         true,
    },
    "editor": {
        ArticlesRead:      true,
        ArticlesCreate:    true,
        ArticlesEditOwn:   true,
        ArticlesEditAny:   true,
        ArticlesDeleteOwn: true,
        ArticlesPublish:   true,
        UsersRead:         true,
    },
    "admin": allPermissions(), // all permissions
}

// allPermissions: every known permission (equivalent of "p for p in Permission")
func allPermissions() map[Permission]bool {
    return map[Permission]bool{
        ArticlesRead: true, ArticlesCreate: true, ArticlesEditOwn: true,
        ArticlesEditAny: true, ArticlesDeleteOwn: true, ArticlesDeleteAny: true,
        ArticlesPublish: true, UsersRead: true, UsersManage: true, AdminAccess: true,
    }
}

func getUserPermissions(user User) map[Permission]bool {
    // Collect all permissions from all of the user's roles
    permissions := map[Permission]bool{}
    for _, role := range user.Roles {
        for p := range rolePermissions[role] {
            permissions[p] = true
        }
    }
    return permissions
}

func hasPermission(user User, permission Permission) bool {
    return getUserPermissions(user)[permission]
}

// Middleware for endpoints
func requirePermission(permission Permission) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !currentUser(r).IsAuthenticated {
                writeJSON(w, http.StatusUnauthorized,
                    map[string]string{"error": "Authentication required"})
                return
            }
            if !hasPermission(currentUser(r), permission) {
                writeJSON(w, http.StatusForbidden,
                    map[string]string{"error": "Insufficient permissions"})
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// Usage on endpoints
http.Handle("/admin/users", requirePermission(UsersManage)(http.HandlerFunc(
    func(w http.ResponseWriter, r *http.Request) {
        writeJSON(w, http.StatusOK, listUsers())
    })))

http.Handle("/articles", requirePermission(ArticlesCreate)(http.HandlerFunc(
    func(w http.ResponseWriter, r *http.Request) {
        // ...
    })))

Attribute-Based Access Control (ABAC) #

ABAC makes decisions based on attribute combinations: user attributes (department, level, location), resource attributes (classification, owner, status), and environment attributes (time, IP, device). More flexible than RBAC but more complex.

// ABAC — decisions based on attributes, not just roles
func canAccessDocument(user User, doc Document, action string, ctx map[string]any) (bool, string) {
    // Evaluate whether the user can perform the action on the document
    // based on attribute combinations.
    if ctx == nil {
        ctx = map[string]any{}
    }

    // Policy: confidential documents can only be read by
    // users from the same department or manager level and above
    if doc.Classification == "confidential" && action == "read" {
        if user.Department != doc.Department {
            if user.Level < "manager" {
                return false, "Confidential documents are restricted to the department"
            }
        }
    }

    // Policy: documents can only be edited by owners or editors
    if action == "edit" {
        if user.ID != doc.OwnerID {
            if !contains(user.Roles, "editor") {
                return false, "Only owners or editors can edit"
            }
        }
    }

    // Policy: no one can delete published documents
    // except admins
    if action == "delete" && doc.Status == "published" {
        if !contains(user.Roles, "admin") {
            return false, "Published documents can't be deleted"
        }
    }

    // Policy: access outside working hours is only for non-sensitive documents
    if hour, ok := ctx["hour"].(int); ok && (hour < 8 || hour > 18) {
        if doc.Classification == "confidential" || doc.Classification == "restricted" {
            return false, "Sensitive documents can't be accessed outside working hours"
        }
    }

    return true, ""
}

Relationship-Based Access Control (ReBAC) #

ReBAC determines access based on relationships between entities — suitable for systems where permissions follow hierarchical or graph structures. Google Zanzibar (used by Google Drive, YouTube, etc.) is a well-known ReBAC implementation.

ReBAC example for a document sharing system:

flowchart LR
    doc_report["doc:report"] -->|"owner"| user_ali["user:ali"]
    doc_report -->|"editor"| user_budi["user:budi"]
    doc_report -->|"viewer"| group_marketing["group:marketing"]
    group_marketing -->|"member"| user_citra["user:citra"]

Question: can user:citra view doc:report?

Graph traversal:

flowchart LR
    user_citra["user:citra"] -->|"member of"| group_marketing["group:marketing"]
    group_marketing -->|"viewer of"| doc_report["doc:report"]

→ Yes, citra can view (viewer access via group membership)

Question: can user:budi edit doc:report? user:budi ──editor of──→ doc:report → Yes, directly

Question: can user:budi delete doc:report? user:budi is only an editor, not the owner → No (deletion requires owner or admin)


The Most Important Principle: Deny by Default #

Deny by default means: if no rule explicitly allows it, access is denied. This is the most fundamental principle and the most frequently violated in authorization implementations.

// ANTI-PATTERN: allow by default (allow unless something forbids)
func canAccess(user User, resource Resource) bool {
    if user.IsBanned {
        return false
    }
    if resource.IsPrivate && user.ID != resource.OwnerID {
        return false
    }
    return true // default: allow
}

// Problem: if a new condition isn't handled (new resource,
// new role, new status), the default is to ALLOW.
// A new bug → a new security hole

// CORRECT: deny by default
func canAccessSafe(user User, resource Resource) bool {
    // Only allow if an explicit condition permits it
    if resource.IsPublic {
        return true
    }
    if user.ID == resource.OwnerID {
        return true
    }
    if contains(user.Roles, "admin") {
        return true
    }
    if resource.SharedWith != nil && contains(resource.SharedWith, user.ID) {
        return true
    }
    return false // default: DENY
}

// With deny by default:
// New resources without rules → automatically denied
// New unconfigured roles → no access at all
// New bugs → fail secure, not fail open
flowchart TD
    A[Request comes in] --> B{User authenticated?}
    B --> |No| C[401 Unauthorized]
    B --> |Yes| D{"Explicit rule\nallowing this?"}
    D --> |No| E["403 Forbidden\nDeny by Default"]
    D --> |Yes — check all conditions| F{"All conditions\nmet?"}
    F --> |No| E
    F --> |Yes| G[Process request ✓]
    G --> H["Audit log:\nWho, what, when, which resource"]

IDOR: The Most Common Authorization Hole #

Insecure Direct Object Reference (IDOR) happens when applications expose internal identifiers (database IDs, file paths) and don’t validate that the requesting user is authorized to access the resource with that identifier.

// ANTI-PATTERN: no ownership validation
// GET /orders/{orderID}
func getOrder(w http.ResponseWriter, r *http.Request) {
    orderID := parseID(r)
    order := findOrderByID(orderID)
    writeJSON(w, http.StatusOK, order)
    // Users can access anyone's orders just by changing order_id:
    // GET /orders/1001 → GET /orders/1002 → GET /orders/1003
}

// A subtler ANTI-PATTERN: only checking roles, not ownership
// PUT /orders/{orderID}
func updateOrder(w http.ResponseWriter, r *http.Request) {
    orderID := parseID(r)
    order := findOrderByID(orderID)
    // The EDIT_OWN permission exists, but no check whether this is THEIR order!
    applyOrderUpdate(order, r)
    writeJSON(w, http.StatusOK, order)
}

// CORRECT: explicitly validate ownership
func getOrderSafe(w http.ResponseWriter, r *http.Request) {
    orderID := parseID(r)
    order := findOrderByID(orderID)

    // Check ownership BEFORE returning data
    if order.UserID != currentUser(r).ID {
        // Admins can see everything — check whether the user is an admin
        if !hasPermission(currentUser(r), PermissionOrdersReadAny) {
            http.Error(w, "Forbidden — this isn't yours", http.StatusForbidden)
            return
        }
    }
    writeJSON(w, http.StatusOK, order)
}

// A cleaner pattern: scoped queries
func getOrderScoped(w http.ResponseWriter, r *http.Request) {
    orderID := parseID(r)
    var order *Order
    if hasPermission(currentUser(r), PermissionOrdersReadAny) {
        order = findOrderByID(orderID)
    } else {
        // The query is already scoped by user_id — getting someone else's order is impossible
        order = findOrderByIDAndUser(orderID, currentUser(r).ID)
    }
    writeJSON(w, http.StatusOK, order)
}
Common IDOR patterns and their mitigations:

  IDOR via numeric IDs:
  GET /users/123 → can access /users/124
  ✓ Mitigation: validate ownership, or use UUIDs

  IDOR via file paths:
  GET /files/uploads/invoice_ali.pdf
  → try /files/uploads/invoice_budi.pdf
  ✓ Mitigation: store metadata in the DB, serve files through
    ownership-validating endpoints, don't expose raw paths

  IDOR via predictable URLs:
  /reports/monthly-2025-01.pdf
  → /reports/monthly-2025-02.pdf
  ✓ Mitigation: generate signed URLs or tokens bound to users

  IDOR via APIs returning everything:
  GET /api/users → returns all users (should only return your own profile)
  ✓ Mitigation: scope queries based on the authenticated user

Least Privilege: Only Grant What’s Needed #

The Principle of Least Privilege (PoLP) states that every entity (user, service, process) should only have the minimum access needed to perform its tasks — nothing more.

Least privilege applied at every level:

  User level:
  → Editors don't need to delete users
  → Staff don't need to see financial data
  → API consumers don't need access to every endpoint

  Service level (microservices):
  → Service A can only read, not write to database B
  → Notification services don't need payment data access
  → Each service has its own database credentials with minimal privileges

  Database level (as discussed in SQL Injection):
  → App users: SELECT, INSERT, UPDATE, DELETE only
  → Migration users: + CREATE, ALTER, DROP
  → Backup users: SELECT only

  API key level:
  → Give specific scopes: "read:orders" not "full_access"
  → Environment-scoped keys: production keys can't be used in staging
// Scoped API key implementation
type APIKey struct {
    ID         int
    KeyHash    string
    Name       string
    UserID     int
    Scopes     []string // ["read:orders", "write:orders"]
    ExpiresAt  time.Time
    LastUsedAt time.Time
    CreatedAt  time.Time
}

func validateAPIKeyScope(apiKey APIKey, requiredScope string) bool {
    // Check whether the API key has the required scope.
    if !apiKey.ExpiresAt.IsZero() && apiKey.ExpiresAt.Before(time.Now()) {
        return false
    }

    // Exact match or wildcard
    prefix := strings.Split(requiredScope, ":")[0] + ":*"
    for _, scope := range apiKey.Scopes {
        if scope == requiredScope || scope == "*" || scope == prefix {
            return true
        }
    }
    return false
}

// Middleware for endpoints requiring a specific scope
func requireAPIScope(scope string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            apiKey := getCurrentAPIKey(r)
            if apiKey == nil {
                writeJSON(w, http.StatusUnauthorized,
                    map[string]string{"error": "API key required"})
                return
            }
            if !validateAPIKeyScope(*apiKey, scope) {
                writeJSON(w, http.StatusForbidden,
                    map[string]string{"error": "Missing required scope: " + scope})
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// Usage on endpoints
// http.Handle("/api/orders", requireAPIScope("read:orders")(http.HandlerFunc(
//     func(w http.ResponseWriter, r *http.Request) {
//         // Only API keys with the "read:orders" or "read:*" scope can access
//     })))

Resource-Level Authorization: The Ownership Validation Pattern #

When permissions depend on the relationship between users and resources (not just global roles), ownership validation should become a standard pattern.

// A reusable ownership validation abstraction
type ResourceAccessPolicy interface {
    CanRead(user User, resource any) bool
    CanEdit(user User, resource any) bool
    CanDelete(user User, resource any) bool
}

type ArticleAccessPolicy struct{}

func (ArticleAccessPolicy) CanRead(user User, article *Article) bool {
    if article.Status == "published" {
        return true // published articles are readable by anyone
    }
    return article.AuthorID == user.ID || hasPermission(user, PermissionArticlesEditAny)
}

func (ArticleAccessPolicy) CanEdit(user User, article *Article) bool {
    if hasPermission(user, PermissionArticlesEditAny) {
        return true
    }
    if hasPermission(user, PermissionArticlesEditOwn) {
        return article.AuthorID == user.ID
    }
    return false
}

func (ArticleAccessPolicy) CanDelete(user User, article *Article) bool {
    if hasPermission(user, PermissionArticlesDeleteAny) {
        return true
    }
    if hasPermission(user, PermissionArticlesDeleteOwn) {
        return article.AuthorID == user.ID
    }
    return false
}

func (ArticleAccessPolicy) CanPublish(user User, article *Article) bool {
    return hasPermission(user, PermissionArticlesPublish)
}

// Policy registry
var policies = map[reflect.Type]ResourceAccessPolicy{
    reflect.TypeOf(Article{}): ArticleAccessPolicy{},
}

func authorize(user User, resource any, action string) bool {
    // Check whether the user can perform the action on the resource.
    policy, ok := policies[reflect.TypeOf(resource)]
    if !ok {
        return false // Deny by default if no policy exists
    }

    switch action {
    case "read":
        return policy.CanRead(user, resource)
    case "edit":
        return policy.CanEdit(user, resource)
    case "delete":
        return policy.CanDelete(user, resource)
    default:
        return false // Unknown action → deny
    }
}

// Usage on endpoints
// PUT /articles/{articleID}
func updateArticle(w http.ResponseWriter, r *http.Request) {
    article := findArticleByID(parseID(r))

    if !authorize(currentUser(r), article, "edit") {
        http.Error(w, "Forbidden", http.StatusForbidden)
        return
    }

    applyArticleUpdate(article, r)
    writeJSON(w, http.StatusOK, article)
}

Privilege Escalation: A Hole Often Missed #

Privilege escalation happens when users gain higher access than they should — either through wrong design or bugs in authorization logic.

// ANTI-PATTERN: an endpoint letting users change their own roles
// PUT /users/{userID}
func updateUser(w http.ResponseWriter, r *http.Request) {
    userID := parseID(r)
    if userID != currentUser(r).ID {
        http.Error(w, "Forbidden", http.StatusForbidden)
        return
    }

    user := findUserByID(userID)
    applyUserUpdate(user, r) // ← users can send {"roles": ["admin"]}!
    saveUser(user)
    writeJSON(w, http.StatusOK, user)
}

// CORRECT: explicitly restrict which fields users can change
func updateUserSafe(w http.ResponseWriter, r *http.Request) {
    userID := parseID(r)
    if userID != currentUser(r).ID {
        http.Error(w, "Forbidden", http.StatusForbidden)
        return
    }

    user := findUserByID(userID)

    // Whitelist the fields regular users may change
    allowedFields := map[string]bool{"name": true, "bio": true,
        "avatar_url": true, "preferences": true}
    updateData := filterFields(body(r), allowedFields)

    // Sensitive fields can only be changed by admins
    if hasPermission(currentUser(r), PermissionUsersManage) {
        sensitiveFields := map[string]bool{"roles": true,
            "is_active": true, "email_verified": true}
        updateData = merge(updateData, filterFields(body(r), sensitiveFields))
    }

    applyUserUpdate(user, updateData)
    saveUser(user)
    writeJSON(w, http.StatusOK, user)
}
Common privilege escalation patterns:

  Mass assignment — model objects updated directly from request bodies
  ✓ Mitigation: whitelist updatable fields

  Role manipulation — profile-change endpoints not filtering role fields
  ✓ Mitigation: roles and permissions can only be changed by admins

  Indirect escalation — users creating entities that inherit high privileges
  Example: a user creates a group then adds themselves to an admin group
  ✓ Mitigation: validate permissions when creating relationships, not just when reading

  Token scope inflation — JWTs that can be modified
  ✓ Mitigation: verify JWT signatures, don't trust payloads without verification

Authorization Logging and Audit Trails #

Every significant authorization decision should be logged — not just for debugging, but for compliance, forensics, and attack detection.

import "log/slog"

var securityLogger = slog.Default().With("logger", "security.authorization")

func logAuthorizationDecision(
    userID int,
    action, resourceType, resourceID string,
    granted bool,
    reason string,
) {
    // Log every authorization decision.
    securityLogger.Info("Authorization decision",
        "user_id", userID,
        "action", action,
        "resource_type", resourceType,
        "resource_id", resourceID,
        "granted", granted,
        "reason", reason,
        "timestamp", time.Now().UTC().Format(time.RFC3339),
        "request_id", gRequestID,
        "ip", remoteIP,
    )
}

// Example usage in a policy
func (ArticleAccessPolicy) CanDelete(user User, article *Article) bool {
    if hasPermission(user, PermissionArticlesDeleteAny) {
        logAuthorizationDecision(user.ID, "delete", "article", article.ID, true,
            "has articles:delete_any permission")
        return true
    }

    if hasPermission(user, PermissionArticlesDeleteOwn) && article.AuthorID == user.ID {
        logAuthorizationDecision(user.ID, "delete", "article", article.ID, true,
            "owner with articles:delete_own permission")
        return true
    }

    logAuthorizationDecision(user.ID, "delete", "article", article.ID, false,
        "insufficient permission")
    return false
}
What needs logging for effective audit trails:

  Every access to sensitive resources (financial data, personal data)
  Every "denied" decision — could be probes or attacks
  Every permission or role change
  Every significant admin action (user deletion, configuration changes)

  A useful format for analysis:
  {
    "timestamp": "2025-06-01T14:23:45Z",
    "event_type": "authorization",
    "user_id": 42,
    "user_email": "[email protected]",
    "action": "delete",
    "resource_type": "article",
    "resource_id": "1234",
    "granted": false,
    "reason": "not owner, missing articles:delete_any",
    "ip": "203.x.x.x",
    "request_id": "req_abc123"
  }

Anti-Patterns to Avoid #

// ✗ Anti-pattern 1: authorization only in the frontend
// A frontend-only role check (e.g. hiding buttons or routes) can be
// bypassed by manipulating the client.
// JavaScript can be manipulated → change the role in the console → access the admin panel
// Backends must ALWAYS validate authorization, not just frontends

// ✗ Anti-pattern 2: guessable IDs without ownership checks
// GET /invoices/{invoiceID}/pdf
func downloadInvoice(w http.ResponseWriter, r *http.Request) {
    invoice := findInvoiceByID(parseID(r))
    sendFile(w, invoice.PDFPath)
    // Users can try /invoices/1, /invoices/2, /invoices/3 to download every invoice
}

// ✓ Solution: validate ownership OR use signed URLs
func downloadInvoiceSafe(w http.ResponseWriter, r *http.Request) {
    // only this user's invoices
    invoice := findInvoiceByIDAndUser(parseID(r), currentUser(r).ID)
    sendFile(w, invoice.PDFPath)
}

// ────────────────────────────────────────────────────────────────────────────

// ✗ Anti-pattern 3: inconsistent permission checks across places
// In controller A: check whether the user is the owner
// In controller B (different endpoint, same resource): forgot the check
// → IDOR in controller B

// ✓ Solution: centralize authorization in policy objects or services
// Authorization logic lives in ONE place — every endpoint uses it

// ────────────────────────────────────────────────────────────────────────────

// ✗ Anti-pattern 4: soft deletes not filtered in authorization
// GET /articles/{articleID}
func getArticle(w http.ResponseWriter, r *http.Request) {
    article := findArticleByID(parseID(r))
    // Deleted articles (soft delete) are still accessible!
    writeJSON(w, http.StatusOK, article)
}

// ✓ Solution: filter soft-deleted records
// article := findArticleByIDAndDeletedAt(parseID(r), nil) // only non-deleted articles

// ────────────────────────────────────────────────────────────────────────────

// ✗ Anti-pattern 5: no rate limiting on authorization-failing endpoints
// Attackers can mass-probe IDOR without detection:
// for i in range(1, 1000000): GET /orders/{i}

// ✓ Solution: log and rate limit based on the number of 403 responses per user

Authorization Checklist #

MODEL & DESIGN:
  □ An authorization model chosen (RBAC/ABAC/ReBAC) per needs
  □ Deny by default applied — no "allow unless something forbids"
  □ Permissions defined with adequate granularity (not just is_admin)
  □ Authorization logic centralized — not scattered across many controllers

OWNERSHIP VALIDATION:
  □ Every endpoint accessing a specific resource validates ownership
  □ Queries scoped by the authenticated user (not filtered after fetching)
  □ Soft-deleted records inaccessible even when IDs are known

IDOR PREVENTION:
  □ No endpoints return resources by ID without ownership checks
  □ File downloads go through access-validating endpoints, not direct paths
  □ Resource IDs in URLs don't grant automatic access

PRIVILEGE ESCALATION PREVENTION:
  □ User-updatable fields restricted with explicit whitelists
  □ Users can't change their own roles or permissions
  □ Admin endpoints protected with strict permission checks

API & INTEGRATION:
  □ API keys have limited scopes
  □ Every API key's usage audited
  □ Service-to-service auth also uses least privilege principles

AUDIT & MONITORING:
  □ Every access to sensitive resources logged
  □ Every authorization denial logged
  □ Permission and role changes logged
  □ Alerts when many 403s come from one user (probe attacks)
  □ Audit logs unmodifiable by regular users

Summary #

  • Deny by default is the most fundamental principle — if no explicit rule allows it, deny. This ensures new resources or roles don’t automatically gain access.
  • RBAC fits most applications — users have roles, roles have permissions. Easy to understand and implement. ABAC and ReBAC for more complex needs.
  • IDOR is the most common authorization hole — always validate that users accessing a resource are actually entitled to it; it’s not enough to just ensure users are logged in.
  • Centralize authorization logic — don’t scatter it across many controllers. One policy class per resource type, all controllers use it. Inconsistency = holes.
  • Scoped queries are safer than post-fetch filteringfilter_by(id=X, user_id=current_user.id) is safer than querying everything then checking ownership, because the database directly enforces the relationship.
  • Least privilege at every level — users only get needed permissions, API keys only have required scopes, services only access relevant resources.
  • Whitelist updatable fields — mass assignment into models from request bodies is a privilege escalation source. Only allow fields that genuinely may change.
  • Frontend authorization is only for UX — all real decisions must happen in the backend. Frontends hiding buttons aren’t real authorization.
  • Authorization audit trails are an investment — log every denial, every sensitive resource access, and every permission change. This data is critical for forensics and compliance.
  • Rate limit 403 responses to detect IDOR probing — attackers mass-probing IDOR will generate many 403s. Detect and block this pattern.
#

← Previous: Authentication   Next: Input Validation

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact