# Business Logic Security Detection Module

> Security detection based on scheduled tasks, configuration management, and other complex business workflows
> Addressing business logic security risks in enterprise applications

## Overview

Business logic flaws exploit intended functionality to violate domain invariants: transfer money without paying, exceed limits, retain privileges after downgrade, or bypass approval workflows. Unlike injection vulnerabilities that require payloads, business logic flaws require understanding the business domain and its invariants.

**Critical**: Business logic security is the enforcement of domain invariants under adversarial sequencing, timing, and inputs. If any step trusts the client or prior steps, expect abuse.

---

## Systematic Methodology

### 1. Actor x Action x Resource Matrix

Build a comprehensive matrix to identify authorization gaps:

**Actors (Roles):**
- Unauthenticated users
- Basic/Free users
- Premium/Paid users
- Trial users
- Staff/Support
- Admin/Super-admin
- Cross-tenant actors (in multi-tenant systems)

**Actions:**
- Create, Read, Update, Delete (CRUD)
- State transitions (approve, reject, cancel, refund)
- Calculations (pricing, discounts, quotas)
- Background operations (jobs, webhooks, sagas)

**Resources:**
- User accounts, profiles, settings
- Financial entities (orders, payments, refunds, credits)
- Content (posts, files, documents)
- Configuration (settings, features, limits)
- Cross-tenant resources

**Detection Pattern:**
```java
// For each endpoint, ask:
// 1. Which actors can access this action?
// 2. Is there server-side validation for actor-resource ownership?
// 3. Can lower-privileged actors access higher-privileged actions?

@PostMapping("/admin/users/{id}/promote")
public Response promoteToAdmin(@PathVariable Long id) {
    // Missing: Who can call this?
    // Missing: Can user promote themselves?
    // Missing: Can non-admin call this endpoint?
    userService.setRole(id, "ADMIN");
}
```

### 2. State Machine Enumeration

For each critical workflow, enumerate:
- **States**: Draft, Pending, Approved, Completed, Cancelled, Refunded
- **Transitions**: Valid state change paths
- **Pre-conditions**: What must be true before a transition
- **Post-conditions**: What must be true after a transition
- **Invariants**: Rules that must always hold

**Example: Order State Machine**
```
States: Created → PendingPayment → Paid → Shipped → Delivered → [Cancelled/Refunded]

Invariants:
- Conservation of value: Sum(payments) - Sum(refunds) = Order total
- Monotonicity: Created timestamp < Paid timestamp < Shipped timestamp
- Uniqueness: One active order per cart session
- Refund constraint: Total refunds <= Total captured amount
```

**Detection Pattern:**
```java
// State transition without pre-condition check
public void shipOrder(Long orderId) {
    // Missing: Check if order is in "Paid" state
    // Missing: Check if order hasn't been cancelled
    // Missing: Check if inventory is still available
    orderRepo.updateStatus(orderId, OrderStatus.SHIPPED);
}

// Safe: Validate pre-conditions
public void shipOrder(Long orderId) {
    Order order = orderRepo.findByIdForUpdate(orderId);

    if (order.getStatus() != OrderStatus.PAID) {
        throw new IllegalStateException("Order not paid");
    }

    if (order.isCancelled()) {
        throw new IllegalStateException("Order cancelled");
    }

    if (!inventory.reserve(order.getItems())) {
        throw new BusinessException("Insufficient inventory");
    }

    order.setStatus(OrderStatus.SHIPPED);
    order.setShippedAt(Instant.now());
    orderRepo.save(order);
}
```

### 3. Invariant Validation

**Conservation of Value:**
```java
// Financial transactions must balance
// Detect: Ledger entries that don't sum to zero
// Detect: Refunds exceeding captured amounts

// No conservation check
public void refund(Long orderId, BigDecimal amount) {
    refundService.process(orderId, amount);
}

// Enforce conservation
public void refund(Long orderId, BigDecimal amount) {
    Order order = orderRepo.findById(orderId);
    BigDecimal totalRefunded = refundRepo.sumByOrderId(orderId);

    if (totalRefunded.add(amount).compareTo(order.getTotalPaid()) > 0) {
        throw new BusinessException("Refund exceeds paid amount");
    }

    refundService.process(orderId, amount);
}
```

**Uniqueness:**
```java
// Single-use tokens, unique coupon codes, one active subscription
// Detect: Missing unique constraints in database
// Detect: Check-then-act patterns without atomicity

// Race condition on uniqueness
if (!couponRepo.existsByCode(code)) {
    couponRepo.save(new Coupon(code));  // Race window!
}

// Database-enforced uniqueness
@Entity
public class Coupon {
    @Column(unique = true, nullable = false)
    private String code;
}
```

**Monotonicity:**
```java
// Timestamps, version numbers, sequence IDs should only increase
// Detect: Backward time travel, version rollback

// No monotonicity check
public void updateVersion(Long id, int newVersion) {
    entity.setVersion(newVersion);  // Could go backward!
}

// Enforce monotonicity
public void updateVersion(Long id, int newVersion) {
    Entity entity = repo.findById(id);
    if (newVersion <= entity.getVersion()) {
        throw new BusinessException("Version must increase");
    }
    entity.setVersion(newVersion);
}
```

### 4. Test Attack Scenarios

**Step Skipping:**
```java
// Can you call step 3 without completing steps 1 and 2?
// Example: Capture payment without authorization
// Example: Ship order without payment

// Detect: Direct API calls to finalize/complete endpoints
// Detect: Missing state validation
```

**Step Repetition:**
```java
// Can you repeat a step that should only happen once?
// Example: Apply same coupon code multiple times
// Example: Redeem gift card multiple times

// Detect: Missing idempotency controls
// Detect: No "used" flag or state tracking
```

**Step Reordering:**
```java
// Can you execute steps out of order?
// Example: Refund before capture
// Example: Cancel after shipment

// Detect: Missing precondition checks
// Detect: State machine not enforced
```

**Late Mutation:**
```java
// Can you modify inputs after validation but before commit?
// Example: Change price after approval
// Example: Swap product after inventory check

// Detect: TOCTOU (Time-of-check Time-of-use) gaps
// Detect: Validation separated from action
```

---

## High-Value Target Categories

### 1. Financial Logic

**Pricing & Discounts:**
```java
// Client-computed total accepted
@PostMapping("/checkout")
public Order checkout(@RequestBody CheckoutRequest req) {
    // Trusts req.total from client!
    return paymentService.charge(req.getTotal());
}

// Server recomputes everything
@PostMapping("/checkout")
public Order checkout(@RequestBody CheckoutRequest req) {
    BigDecimal total = pricingEngine.calculate(req.getItems());
    BigDecimal discount = discountEngine.apply(req.getCouponCode(), total);
    BigDecimal tax = taxService.calculate(total.subtract(discount), req.getAddress());
    BigDecimal finalTotal = total.subtract(discount).add(tax);

    return paymentService.charge(finalTotal);
}
```

**Discount Stacking:**
```java
// No mutual exclusivity check
public BigDecimal applyDiscount(List<String> couponCodes, BigDecimal amount) {
    BigDecimal discounted = amount;
    for (String code : couponCodes) {
        discounted = discounted.multiply(getDiscountRate(code));
    }
    return discounted;  // Could stack incompatible discounts!
}

// Enforce mutual exclusivity
public BigDecimal applyDiscount(List<String> couponCodes, BigDecimal amount) {
    if (couponCodes.size() > 1) {
        List<String> categories = couponCodes.stream()
            .map(this::getCouponCategory)
            .distinct()
            .collect(Collectors.toList());

        if (categories.size() > 1) {
            throw new BusinessException("Cannot stack coupons from different categories");
        }
    }

    // Apply best discount only
    return couponCodes.stream()
        .map(code -> calculateDiscount(code, amount))
        .min(Comparator.naturalOrder())
        .orElse(amount);
}
```

**Refund Logic:**
```java
// Multiple refund paths without coordination
// Path 1: UI refund
// Path 2: Support tool refund
// Path 3: Automated refund (chargeback)
// Result: Double refund possible

// Centralized refund ledger
public void refund(Long orderId, BigDecimal amount, RefundSource source) {
    synchronized (getLock(orderId)) {  // Or use database lock
        BigDecimal totalRefunded = refundLedger.sumByOrderId(orderId);
        BigDecimal totalCaptured = paymentLedger.sumCapturedByOrderId(orderId);

        if (totalRefunded.add(amount).compareTo(totalCaptured) > 0) {
            throw new BusinessException("Total refunds would exceed captured amount");
        }

        refundLedger.create(orderId, amount, source);
        paymentGateway.refund(orderId, amount);
    }
}
```

### 2. Quotas & Limits

```java
// Non-atomic quota check
int usage = quotaService.getUsage(userId);
if (usage < LIMIT) {
    // Race window - quota can be exceeded
    processRequest();
    quotaService.increment(userId);
}

// Atomic quota enforcement
long newUsage = quotaService.incrementAndGet(userId);
if (newUsage > LIMIT) {
    quotaService.decrement(userId);  // Rollback
    throw new QuotaExceededException();
}
processRequest();
```

**Limit Slicing:**
```java
// Per-transaction limit without total limit
// User sends 100 transactions of $99 each to bypass $100 limit

// Both per-transaction and total limits
public void transfer(Long fromUserId, Long toUserId, BigDecimal amount) {
    if (amount.compareTo(PER_TRANSACTION_LIMIT) > 0) {
        throw new BusinessException("Per-transaction limit exceeded");
    }

    BigDecimal dailyTotal = transferRepo.sumTodayByUser(fromUserId);
    if (dailyTotal.add(amount).compareTo(DAILY_LIMIT) > 0) {
        throw new BusinessException("Daily limit exceeded");
    }

    executeTransfer(fromUserId, toUserId, amount);
}
```

### 3. Subscription & Account Lifecycle

```java
// Role retention after downgrade
public void downgradeSubscription(Long userId) {
    subscriptionRepo.updateTier(userId, "FREE");
    // Missing: Remove premium features/permissions
    // User retains premium capabilities!
}

// Clean state transition
@Transactional
public void downgradeSubscription(Long userId) {
    User user = userRepo.findByIdForUpdate(userId);

    // Check pre-conditions
    if (user.getSubscription().getTier() == Tier.FREE) {
        throw new BusinessException("Already on free tier");
    }

    // Transition state
    user.getSubscription().setTier(Tier.FREE);

    // Enforce post-conditions
    user.setMaxProjects(FREE_TIER_LIMIT);
    user.getPremiumFeatures().clear();
    featureGateService.revokePremiumAccess(userId);

    // Delete excess resources
    projectService.deleteExcessProjects(userId, FREE_TIER_LIMIT);

    userRepo.save(user);
    auditLog.record("SUBSCRIPTION_DOWNGRADED", userId);
}
```

### 4. Multi-Tenant Isolation

```java
// Missing tenant boundary check
public List<Document> search(String query) {
    return documentRepo.search(query);  // Leaks across tenants!
}

// Tenant-scoped query
public List<Document> search(String query) {
    String tenantId = SecurityContext.getCurrentTenantId();
    return documentRepo.searchByTenant(tenantId, query);
}

// Aggregate operations without tenant filter
public void resetAllUserCounters() {
    userRepo.updateAll("counter", 0);  // Affects all tenants!
}

// Tenant-aware operations
public void resetTenantUserCounters(String tenantId) {
    validateTenantAdmin(tenantId);  // Authorization
    userRepo.updateByTenant(tenantId, "counter", 0);
    auditLog.record("BULK_COUNTER_RESET", tenantId);
}
```

---

## Detection Patterns

### Client-Computed Values

```bash
# Grep for endpoints accepting totals, prices, discounts from client
grep -rn "BigDecimal.*total\|Double.*price\|int.*discount" --include="*.java" -B 3

# Look for @RequestBody or @RequestParam receiving financial values
grep -rn "@Request.*total\|@Request.*price\|@Request.*amount" --include="*.java" -A 5
```

**Red Flags:**
- Request DTOs with `total`, `finalPrice`, `discountedAmount` fields
- Server not recalculating pricing
- Comments like "// TODO: validate total"

### Missing State Validation

```bash
# Find state-changing methods without state checks
grep -rn "public.*void.*(complete\|finalize\|approve\|ship\|refund)" --include="*.java" -A 10 | \
grep -v "if.*status\|if.*state\|getStatus()\|getState()"
```

### Idempotency Gaps

```bash
# Find operations that should be idempotent but lack controls
grep -rn "@Post.*charge\|@Post.*refund\|@Post.*apply\|@Post.*redeem" --include="*.java" -A 15 | \
grep -v "idempotency\|idempotent\|@Transactional.*SERIALIZABLE"
```

### Background Job Security

```bash
# Find scheduled tasks and async jobs
grep -rn "@Scheduled\|@Async\|@RabbitListener\|@KafkaListener" --include="*.java" -A 10

# Check if they bypass authorization
grep -rn "@Scheduled" --include="*.java" -A 10 | \
grep -v "checkPermission\|hasRole\|@RequiresPermissions"
```

---

## Risk Pattern Library

### Risk Pattern 1: Scheduled Task Management Business Logic Vulnerability (High Risk)

#### Vulnerable Code Example
```java
// High Risk: Scheduled task creation without business logic validation
@PostMapping("/monitor/job/add")
public AjaxResult addSave(SysJob job) {
    // Missing business logic validation:
    // - Method invocation permission validation
    // - Parameter content security check
    // - Execution frequency limit validation
    return toAjax(jobService.insertJobCron(job));
}

// High Risk: Scheduled task execution without resource limits
public class ScheduleRunnable implements Runnable {
    public void run() {
        // No resource usage limits
        // No execution time limits
        // No exception handling limits
        method.invoke(target, params);
    }
}
```

### Risk Pattern 2: Configuration Management Business Logic Vulnerability (Medium Risk)

#### Vulnerable Code Example
```java
// Medium Risk: System configuration modification without permission validation
@PostMapping("/system/config/edit")
public AjaxResult editSave(SysConfig config) {
    // Missing business logic validation for configuration changes:
    // - Configuration item permission validation
    // - Configuration value format validation
    // - Configuration impact scope assessment
    return toAjax(configService.updateConfig(config));
}
```

### Risk Pattern 3: Data Export Business Logic Vulnerability (Medium Risk)

#### Vulnerable Code Example
```java
// Medium Risk: Data export without permission and scope control
@PostMapping("/system/user/export")
public AjaxResult export(SysUser user) {
    // Missing business logic validation for data export:
    // - Export data scope control
    // - Export frequency limit
    // - Sensitive data filtering
    List<SysUser> list = userService.selectUserList(user);
    return util.exportExcel(list, "user");
}
```

---

## Detection Command Set

### Business Logic Interface Detection
```bash
# 1. Scheduled task management detection
grep -rn "@.*Mapping.*/monitor/job" --include="*.java" -A 15

# 2. Configuration management detection
grep -rn "@.*Mapping.*/system/config" --include="*.java" -A 15

# 3. Data export detection
grep -rn "@.*Mapping.*/export" --include="*.java" -A 15

# 4. Permission control detection
grep -rn "@RequiresPermissions" --include="*.java" -B 2 -A 5

# 5. Business logic validation detection
grep -rn "validate\|check\|verify" --include="*.java" -B 2 -A 2
```

### Business Data Flow Detection
```bash
# 1. Complete path from user input to business logic
grep -rn "@.*Mapping" --include="*.java" | head -20

# 2. Business logic processing chain detection
grep -rn "Service\." --include="*.java" | grep -E "save|update|delete|execute"

# 3. Database operation business logic detection
grep -rn "Mapper\." --include="*.java" -B 3 -A 3
```

---

## Security Fix Solutions

### Fix 1: Business Logic Permission Validation

```java
// Secure: Business logic permission validation
@RequiresPermissions("monitor:job:add")
@PostMapping("/monitor/job/add")
public AjaxResult addSave(SysJob job) {
    // Business logic permission validation
    if (!hasJobCreationPermission(job)) {
        return error("No permission to create this job");
    }

    // Business logic parameter validation
    if (!isValidJobConfiguration(job)) {
        return error("Invalid job configuration");
    }

    // Business logic resource limit validation
    if (!hasSufficientResources(job)) {
        return error("Insufficient resources for this job");
    }

    return toAjax(jobService.insertJobCron(job));
}

private boolean hasJobCreationPermission(SysJob job) {
    // Fine-grained business permission control
    return SecurityUtils.getSubject().isPermitted("job:create:" + job.getJobGroup());
}

private boolean isValidJobConfiguration(SysJob job) {
    // Business logic configuration validation
    return job.getCronExpression() != null &&
           job.getMethodName() != null &&
           isAllowedMethod(job.getMethodName());
}
```

### Fix 2: Business Data Scope Control

```java
// Secure: Business data scope control
@PostMapping("/system/user/export")
public AjaxResult export(SysUser user) {
    // Business data scope control
    if (!hasDataExportPermission(user)) {
        return error("No permission to export this data");
    }

    // Business data filtering
    user = filterSensitiveData(user);

    // Business frequency limit
    if (!checkExportFrequency()) {
        return error("Export frequency limit exceeded");
    }

    List<SysUser> list = userService.selectUserList(user);
    return util.exportExcel(list, "user");
}

private boolean hasDataExportPermission(SysUser user) {
    // Business data permission control
    User currentUser = SecurityUtils.getCurrentUser();
    return currentUser.hasPermission("data:export:" + user.getDeptId());
}
```

### Fix 3: Business Resource Limit Control

```java
// Secure: Business resource limit control
public class ScheduleRunnable implements Runnable {
    private static final long MAX_EXECUTION_TIME = 30000; // 30 seconds
    private static final int MAX_MEMORY_USAGE = 1024; // 1GB

    public void run() {
        long startTime = System.currentTimeMillis();

        try {
            // Execution time limit
            if (System.currentTimeMillis() - startTime > MAX_EXECUTION_TIME) {
                throw new TimeoutException("Execution time exceeded");
            }

            // Memory usage limit
            if (getMemoryUsage() > MAX_MEMORY_USAGE) {
                throw new MemoryLimitException("Memory usage exceeded");
            }

            method.invoke(target, params);

        } catch (Exception e) {
            log.error("Task execution failed", e);
        }
    }
}
```

---

## Risk Rating Matrix

| Risk Type | Severity | Exploit Difficulty | Detection Difficulty | Fix Priority |
|-----------|----------|-------------------|---------------------|-------------|
| Scheduled Task Business Logic | High | Medium | High | Fix Immediately |
| Configuration Management Business Logic | Medium | Medium | Medium | Plan Fix |
| Data Export Business Logic | Medium | High | Medium | Plan Fix |
| Permission Control Business Logic | Medium | Low | Low | Plan Fix |

---

## Security Best Practices

1. **Business Permission Control**: Implement fine-grained business logic permission validation
2. **Data Scope Control**: Strictly limit the scope of business data access and operations
3. **Resource Usage Limits**: Limit resource usage for business operations
4. **Exception Handling Mechanism**: Comprehensive business exception handling and logging
5. **Business Process Audit**: Complete audit trail for business process operations

---

## Detection Priority

### High-Risk Detection Items (Execute Immediately)
- [ ] Scheduled task business logic security detection
- [ ] Missing method invocation permission validation detection
- [ ] Missing resource usage limit detection

### Medium-Risk Detection Items (Plan Execution)
- [ ] Configuration management business logic detection
- [ ] Data export business logic detection
- [ ] Permission control business logic detection

### Basic Detection Items (Routine Execution)
- [ ] Business process data flow tracing
- [ ] Business exception handling mechanism detection
- [ ] Business operation audit log detection

---

## Real-World Case Study: RuoYi Management System Data Permission Filtering

### Case Background
**Project**: RuoYi v3.1
**Module**: Data Permission Filtering (Data Scope)
**Risk**: MyBatis dynamic SQL concatenation security risk
**CVSS**: 6.5 (Medium)

### Vulnerable Code Analysis

#### Mapper XML Configuration
```xml
<!-- SysDeptMapper.xml:38-52 -->
<select id="selectDeptList" parameterType="SysDept" resultMap="SysDeptResult">
    <include refid="selectDeptVo"/>
    where d.del_flag = '0'
    <if test="parentId != null and parentId != 0">
        AND parent_id = #{parentId}
    </if>
    <if test="deptName != null and deptName != ''">
        AND dept_name like concat('%', #{deptName}, '%')  <!-- Safe parameterization -->
    </if>
    <!-- Critical risk point: Using ${} for SQL concatenation -->
    ${params.dataScope}
</select>
```

#### AOP Aspect Implementation
```java
// DataScopeAspect.java:74-105
@Aspect
@Component
public class DataScopeAspect {

    @Before("dataScopePointCut()")
    public void doBefore(JoinPoint point) throws Throwable {
        handleDataScope(point);
    }

    protected void handleDataScope(final JoinPoint joinPoint) {
        // Get annotation
        DataScope controllerDataScope = getAnnotationLog(joinPoint);
        if (controllerDataScope == null) {
            return;
        }

        // Get current user
        SysUser currentUser = ShiroUtils.getSysUser();
        if (currentUser != null && !currentUser.isAdmin()) {
            // Non-admin requires data permission filtering
            dataScopeFilter(joinPoint, currentUser, controllerDataScope.tableAlias());
        }
    }

    public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String alias) {
        StringBuilder sqlString = new StringBuilder();

        for (SysRole role : user.getRoles()) {
            String dataScope = role.getDataScope();

            if (DATA_SCOPE_ALL.equals(dataScope)) {
                // All data permission - clear filter conditions
                sqlString = new StringBuilder();
                break;
            }
            else if (DATA_SCOPE_CUSTOM.equals(dataScope)) {
                // Critical issue: Using string formatting to concatenate SQL
                sqlString.append(StringUtils.format(
                    " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ",
                    alias,           // From annotation, but unsafe design
                    role.getRoleId() // From database, but still string concatenation
                ));
            }
            else if (DATA_SCOPE_DEPT.equals(dataScope)) {
                sqlString.append(StringUtils.format(
                    " OR {}.dept_id = {} ",
                    alias, user.getDeptId()
                ));
            }
        }

        if (StringUtils.isNotBlank(sqlString.toString())) {
            BaseEntity baseEntity = (BaseEntity) joinPoint.getArgs()[0];
            // Concatenated SQL fragment placed in params, then used with ${} in XML
            baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
        }
    }
}
```

#### StringUtils.format Implementation
```java
// StrFormatter.java:30-91
public static String format(final String strPattern, final Object... argArray) {
    // Simple string replacement, no SQL safety handling
    final int strPatternLength = strPattern.length();
    StringBuilder sbuf = new StringBuilder(strPatternLength + 50);

    int handledPosition = 0;
    int delimIndex;

    for (int argIndex = 0; argIndex < argArray.length; argIndex++) {
        delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition);  // Find {}
        if (delimIndex == -1) {
            if (handledPosition == 0) {
                return strPattern;
            } else {
                sbuf.append(strPattern, handledPosition, strPatternLength);
                return sbuf.toString();
            }
        } else {
            sbuf.append(strPattern, handledPosition, delimIndex);
            sbuf.append(Convert.utf8Str(argArray[argIndex]));  // Direct concatenation
            handledPosition = delimIndex + 2;
        }
    }

    sbuf.append(strPattern, handledPosition, strPattern.length());
    return sbuf.toString();
}
```

### Audit Discovery Process

```bash
# 1. Search MyBatis ${} usage
grep -rn '\$\{' --include="*.xml"
# Found: ${params.dataScope} appears in multiple Mappers

# 2. Trace dataScope source
grep -rn "dataScope" --include="*.java"
# Found: DataScopeAspect.java

# 3. Analyze @DataScope annotation usage
grep -rn "@DataScope" --include="*.java"
# Found: Widely used in Service layer

# 4. Check StringUtils.format implementation
grep -rn "public.*format" --include="*.java" -A 20 | grep -i "sql"

# 5. Compare with safe concat usage
grep -rn "concat\(" --include="*.xml"
# Found: Most places correctly use concat('%', #{param}, '%')
```

### Risk Analysis

Although this is not a direct SQL injection (since data sources are relatively trusted), the following issues exist:

#### 1. Design Defect
```
Violates the basic security principle that "all SQL must be parameterized"
Uses string concatenation instead of MyBatis's #{} parameterization
```

#### 2. Potential Risks
```
If annotation configuration can be tampered with (configuration injection)
If other code injection points affect the alias
If database data is compromised (roleId tampered with)
```

#### 3. Maintenance Risk
```
Subsequent development may incorrectly inject user input into params
Code review can easily overlook this "internal" SQL concatenation
Increases the attack surface of the vulnerability
```

### Security Fix Solutions

#### Solution 1: Completely Eliminate ${} (Recommended)

```java
// Mapper interface add parameters
List<SysDept> selectDeptList(@Param("dept") SysDept dept,
                              @Param("dataScopeSql") String dataScopeSql,
                              @Param("dataScopeIds") List<Long> dataScopeIds);

// XML changed to full parameterization
<select id="selectDeptList" resultMap="SysDeptResult">
    <include refid="selectDeptVo"/>
    where d.del_flag = '0'
    <if test="deptName != null and deptName != ''">
        AND dept_name like concat('%', #{deptName}, '%')
    </if>

    <!-- Use foreach for data permission filtering -->
    <if test="dataScopeIds != null and dataScopeIds.size() > 0">
        AND d.dept_id IN
        <foreach collection="dataScopeIds" item="id" open="(" close=")" separator=",">
            #{id}
        </foreach>
    </if>
</select>

// Aspect changed to pass ID list
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String alias) {
    List<Long> allowedDeptIds = new ArrayList<>();

    for (SysRole role : user.getRoles()) {
        String dataScope = role.getDataScope();

        if (DATA_SCOPE_ALL.equals(dataScope)) {
            allowedDeptIds.clear();  // Clear means no filtering
            break;
        }
        else if (DATA_SCOPE_CUSTOM.equals(dataScope)) {
            // Query allowed department IDs for the role
            List<Long> roleDeptIds = deptMapper.selectDeptIdsByRoleId(role.getRoleId());
            allowedDeptIds.addAll(roleDeptIds);
        }
        else if (DATA_SCOPE_DEPT.equals(dataScope)) {
            allowedDeptIds.add(user.getDeptId());
        }
    }

    BaseEntity baseEntity = (BaseEntity) joinPoint.getArgs()[0];
    baseEntity.getParams().put("dataScopeIds", allowedDeptIds);
}
```

#### Solution 2: Alias Whitelist Validation (Second Best)

```java
// If ${} must be used, at least validate alias
private static final Set<String> ALLOWED_ALIASES = new HashSet<>(
    Arrays.asList("d", "u", "r", "dept", "user", "role")
);

public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String alias) {
    // Whitelist validate alias
    if (!ALLOWED_ALIASES.contains(alias)) {
        throw new SecurityException("Invalid table alias: " + alias);
    }

    // Use PreparedStatement style
    StringBuilder sqlString = new StringBuilder();
    List<Object> params = new ArrayList<>();

    for (SysRole role : user.getRoles()) {
        if (DATA_SCOPE_CUSTOM.equals(role.getDataScope())) {
            sqlString.append(" OR ").append(alias)
                     .append(".dept_id IN (SELECT dept_id FROM sys_role_dept WHERE role_id = ?)");
            params.add(role.getRoleId());
        }
    }

    // Note: This approach is still not ideal, but better than direct concatenation
}
```

### Business Logic Detection Checklist

#### Data Permission Control Detection
- [ ] Check if @DataScope annotation usage is correct
- [ ] Verify data permission filtering SQL is parameterized
- [ ] Confirm role permission configuration security
- [ ] Test privilege escalation scenarios
- [ ] Check admin permission bypass logic

#### AOP Aspect Security Detection
- [ ] Check for SQL concatenation operations in aspects
- [ ] Verify security of aspect parameter sources
- [ ] Confirm completeness of aspect exception handling
- [ ] Test aspect behavior in various scenarios

### Key Lessons Learned

1. **Internal Data Also Needs Parameterization**
   - Even data from the database should use parameterized queries
   - String concatenation is never safe

2. **AOP Aspects Are Security Blind Spots**
   - AOP logic is easily overlooked
   - SQL operations in aspects need special attention

3. **Business Logic Complexity Increases Risk**
   - Complex data permission logic is more error-prone
   - Requires comprehensive security test coverage

4. **Code Review Must Be In-Depth**
   - Cannot only look at surface-level CRUD operations
   - Need to trace the complete data flow

---

## Minimal PoC Examples
```bash
# Privilege escalation (IDOR)
curl -H "Authorization: Bearer USER" https://app.example.com/api/orders/2
curl -H "Authorization: Bearer USER" https://app.example.com/api/orders/3

# Data permission AOP SQL concatenation
rg -n "DataScope|dataScope" --glob "*.{java,kt}"

# Flow skip/replay
curl -X POST https://app.example.com/api/checkout -d "step=3"  # Skip prerequisite steps
```

---

Through the detection rules and RuoYi data permission case study in this module, business logic security risks in enterprise applications can be effectively identified, especially security issues in complex business workflows and back-office system operations.
