← 中文资源

简体中文版本

LDAP 注入

LDAP 注入把过滤器元字符注入目录查询,使攻击者扩大搜索范围或绕过认证检查,影响企业认证与目录检索端点。

阅读英文原文

概念概述

LDAP 注入(LDAP Injection)指的是:LDAP 注入把过滤器元字符注入目录查询,使攻击者扩大搜索范围或绕过认证检查,影响企业认证与目录检索端点。

从更严格的定义看,LDAP injection is an attack that exploits unescaped user input in a directory filter expression, altering the query’s logic so it matches unintended entries. 该概念在工程实践中的价值,在于它能帮助团队用统一的语言描述一个具体的质量属性或设计约束,从而在评审与协作中减少歧义。

为什么需要它

It matters for enterprise authentication and directory search endpoints that build filters from user-supplied names or identifiers. 它与 HTTP Response Splitting、Rainbow Tables and Lookup Attacks、Password Salting 等概念经常一起出现,用于解决相近的工程问题。理解它的适用场景,有助于在多种备选方案之间做出有依据的取舍,而不是凭直觉选择技术方案。

常见误解

  • LDAP filters are safe because they are not SQL
  • escaping only special characters is sufficient for every context

需要避免的反模式

  • concatenating a username into a search filter
  • validating input only for length while allowing filter syntax characters

实践要点

  1. Filter metacharacters such as parentheses, asterisks, and logical operators can change the structure of an expression, so a login filter may match more than one identity.
  2. Escape special characters according to the directory specification and prefer APIs that accept a filter template with bound parameters over string building.
  3. Authentication should bind as the user rather than search for a user and compare a stored password, because the search step is a common injection sink.

参考来源

https://owasp.org/www-community/attacks/LDAP_Injection

代码示例

-- 危险示范:拼接用户输入(永远不要这样做)
-- SELECT * FROM products WHERE name = '克" OR "1"="1'
-- ORDER BY 攻击同理:排序字段直接内插

-- 安全形态:值全部走绑定参数
SELECT id, name, price_cents
FROM products
WHERE category = $1
  AND price_cents BETWEEN $2 AND $3
ORDER BY price_cents
LIMIT $4;
-- 绑定:$1='electronics', $2=1000, $3=5000, $4=20

-- IN 列表的动态占位符:结构生成、值绑定
SELECT id, name
FROM products
WHERE id IN ($1, $2, $3);
-- 占位符数量由应用层数组长度生成,值逐个绑定

-- ORDER BY 无法参数化:白名单映射是唯一出路
-- 应用层伪代码:
--   allowed = { 'price': 'price_cents',
--               'name': 'name',
--               'created': 'created_at DESC' }
--   order_sql = allowed[user_input] or fail()
SELECT id, name, price_cents
FROM products
WHERE category = $1
ORDER BY price_cents DESC NULLS LAST;  -- order_sql 来自白名单

-- 标识符必须动态时用格式化函数加引号
SELECT quote_ident('user_input_column');  -- 带引号的安全标识符
import { DataSource, Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity({ name: 'accounts' })
class Account {
  @PrimaryGeneratedColumn() id!: number;
  @Column() email!: string;
  @Column() status!: string;
}

const dataSource = new DataSource({
  type: 'postgres',
  url: process.env.DATABASE_URL,
  entities: [Account],
});

await dataSource.initialize();
const repo = dataSource.getRepository(Account);

async function searchByDomain(domain: string) {
  // LIKE 模式里的通配符要转义,值仍走参数绑定
  const escaped = domain.replace(/[\\%_]/g, (ch) => '\\' + ch);

  return repo
    .createQueryBuilder('account')
    .where('account.email LIKE :pattern', { pattern: '%' + escaped + '%' })
    .andWhere('account.status = :status', { status: 'active' })
    .orderBy('account.id', 'ASC')
    .limit(50)
    .getMany();
}

function rejectUnsafeFragment(input: string): never {
  if (/;|--|\/\*/.test(input)) {
    throw new Error('suspicious SQL fragment rejected');
  }
  throw new Error('raw fragments are not allowed');
}

const matches = await searchByDomain('example.com');
console.log('accounts found:', matches.length);
await dataSource.destroy();