PHP7 Zend 认证学习指南(三)
五、面向对象的 PHP
面向对象的代码比过程代码运行得慢,但是更容易建模和操作复杂的数据结构。PHP 从 3.0 版本开始就支持面向对象编程,从那以后它的对象模型得到了广泛的扩展和改造。
这本书不打算教面向对象编程,而是将重点放在 PHP 的实现上。你应该至少有一些 PHP 编程的经验。
Tip
这是认证考试最重要的三个部分之一。
声明类和实例化对象
使用关键字class声明类。
<?php
class ExampleClass
{
// class code
}
可以使用与变量相同的规则来命名类。您的编码标准将决定您使用的大小写约定。
要从一个类中实例化一个对象,可以使用new关键字:
<?php
$exampleObject = new ExampleClass();
// If you are not passing constructor parameters you can omit the brackets if you choose
$anotherObject = new ExampleClass;
我们稍后将处理细节,但是下面的概要参考表显示了继承和特征的语法和限制。
| 概念 | 句法 | 限制 |
|---|---|---|
| 从一个类继承 | class A extends A_Parent | 类可能只有一个父类 |
| 接口继承 | Interface A extends B, C | 接口可以继承多个接口 |
| 从抽象类继承 | Interface A extends B, C | 接口可以继承多个接口 |
| 实现接口 | class A implements A_Interface | 类可以实现多个接口 |
| 特点 | class Foo { use A_trait; } | 类可以使用多个特征 |
对象分配总是通过引用。
请注意,在下面的示例中,当我们更改复制对象的属性时,原始对象也会随之更改。事实上,这两个变量在内存中占据相同的空间,因为引用是指向原始数据的指针。我们并没有制作该对象的全新副本。
<?php
$a = new stdClass();
$a->property = "Hello World";
// object assignment is by reference
$b = $a;
$b->property = "Assigned by reference";
// $a has also changed because $b is a pointer to $a
var_dump($a);
/*
object(stdClass)#1 (1) {
["property"]=>
string(21) "Assigned by reference"
}
*/
当我们在“使用对象”一节中学习到关键字clone时,我们将会更详细地讨论这个问题。
自动加载类
应该在使用类之前定义它们,但是可以在需要时使用自动加载来加载类。与控制 PHP 在哪里寻找类的编码标准(如 PSR4)一起,这可能是一个不可或缺的特性。
Tip
Zend 考试不会问你关于 PSR4 的问题,但是 FIG 组提出的标准在 PHP 世界里非常重要。
PHP 中的自动加载是通过spl_autoload_register()函数完成的。PHP FIG 组网页上给出了一个符合 PSR4 的实现, 1 但是让我们看一个来自 PHP 手册 2 的更简单的演示作为例子:
<?php
function my_autoloader($class) {
include 'classes/' . $class . '.class.php';
}
spl_autoload_register('my_autoloader');
// Or, using an anonymous function as of PHP 5.3.0
spl_autoload_register(function ($class) {
include 'classes/' . $class . '.class.php';
});
使用spl_autoload_register()可以指定 PHP 在无法加载类时将调用什么函数。您可以在此函数中包含文件,并声明该类。如果 PHP 在这个函数运行后找不到这个类,那么它将抛出一个致命错误。
可见性或访问修饰符
方法或属性的可见性可以通过在声明前加上public、protected或private来设置。
- 可以从任何地方访问公共类成员。
- 受保护的类成员可以从类内部及其子级进行访问。
- 私有类成员只能从类本身内部访问。
如果你没有明确指定可见性,那么它将默认为public。
接口只能包含public方法。任何实现该接口的类都必须匹配该方法的可见性,因此这些方法在其中也将是公共的。
abstract类中的方法可以有任何可见性。扩展抽象类的类中的方法必须具有相同或更少限制的可见性。
实例属性和方法
从类中创建的具体对象也称为实例。当你从一个类中创建一个对象时,你被称为实例化该对象。本节重点介绍属于对象的属性和方法。我们将看看这些是什么,PHP 语法如何工作,命名规则,以及如何使用它们。
性能
通过使用一个可见性修饰符后跟属性名来声明类属性。属性名遵循与变量相同的命名规则。
<?php
class Properties
{
// You do not have to specify a default value
public $email;
// A scalar value is an expression
protected $name = 'Alice';
// An array is an expression
protected $accounts = ['cheque', 'savings'];
// You can use a constant expression as a default value
private $balance = 60 * 5;
}
属性可以初始化为默认值。它们可以用表达式初始化,但不能用函数初始化。
<?php
class BrokenPropertyInit
{
private $lastLogin = time(); // won't run
}
该示例不会运行,因为您无法使用函数初始化 class 属性。
方法
方法是作用域构造中的函数。它们是在函数中通过使用可见性修饰符后跟函数声明来声明的。如果省略可见性修饰符,该方法将具有公共可见性。
<?php
class MethodExample
{
private $name;
// explicitly specified visibility
public function setName($name) {
$this->name = $name;
}
// public visibility by default
function getName($name) {
return $this->name;
}
}
方法可以使用$this伪变量访问非静态对象属性。
$this伪变量在对象中定义,指的是对象本身。
静态方法是在没有实例化对象的情况下声明的,因此$this不可用。
静态方法和属性
将一个方法或属性声明为 static 使得它不需要类的具体实现就可以使用。
因为可以在没有实例化对象的情况下调用静态方法,所以伪变量$this在这些方法中是不可访问的。静态方法和属性可以应用任何可见性修饰符。
您不应该静态地调用非静态方法。这将生成一个弃用警告:
<?php
class A
{
// this is not a static method
public function sayHello()
{
echo "Hello World";
}
}
// Deprecated: Non-static method A::sayHello() should not be called statically
A::sayHello();
引用静态属性或方法是使用范围解析运算符完成的,它是一个双冒号。
<?php
class MyClass
{
// Static functions are declared with the static keyword
public static function sayHello() {
echo "Hello World" . PHP_EOL;
}
public function someFunction() {
// self refers to "this class", like $this refers to an object
self::sayHello();
}
}
// Static functions can be accessed with the scope resolution operator.
MyClass::sayHello(); // Hello World
$object = new MyClass();
$object->someFunction(); // Hello World
当我们从类内部引用一个静态属性时,我们可以使用self、parent或static来引用它。我们将在本章的“后期静态绑定”一节中讨论static关键字。
当从类外部引用静态类成员时,可以用类名作为范围解析运算符的前缀。在前面的例子中,我们用MyClass::sayHello()引用了静态函数。
静态属性
静态属性也用关键字static声明,可以用范围解析操作符访问。
例如:
<?php
class Foo
{
// Static properties are declared with the static keyword
private static $message = 'Hello World';
public function __construct() {
// Static properties can be accessed with the scope resolution operator.
echo self::$message;
}
}
$foo = new Foo; // Hello World
echo Foo::$message; // PHP Fatal error: Cannot access private property Foo::$message
在这个例子中,我们使用关键字self访问构造函数中的static属性。为了演示静态属性可以应用任何可见性,我们尝试从类外部访问它,并收到一个致命错误。
使用对象
这是本章非常重要的一节,你应该密切注意细节。我们将介绍“浅”拷贝和“深”拷贝的区别,并看看数组变量是如何被转换成其他变量类型的。我们将看到如何存储一个对象供以后使用(或者将它传递给另一个程序),还将看到一些通过别名类名可以玩的把戏。
复制对象
就像赋值一样,PHP 总是通过引用传递对象。我们不是制作对象的整个副本,而是说“数据可以在这个位置找到”。我们将在本书的“内存管理”一节中更多地讨论 PHP 内存分配。
如果要创建对象的副本,必须使用clone()关键字。
<?php
// creating a shallow copy of an object
$objectCopy = clone $originalObject;
PHP 将创建该对象的浅层副本。在浅层复制中,如果源包含对变量或其他对象的引用,那么这些引用将被复制到新对象中。这意味着原始对象和克隆对象共享对同一目标对象的引用。
相比之下,深层副本创建被引用对象的新版本,并将对这些对象的引用插入到克隆对象中。这种方法速度更慢,成本更高,因为它需要创建更多的对象。克隆的对象将包含对原始对象引用的对象的新副本的引用。
当克隆一个对象时,PHP 会尝试执行对象中的__clone()方法。您可以重写此方法,以包含您自己的克隆对象行为。不能直接调用此方法。
Tip
如果你想要一个对象的深度克隆,你可以在神奇的方法__clone()中实现这个逻辑。
序列化对象
对象序列化是通过serialize()和unserialize()函数完成的。这些函数支持任何类型的 PHP 变量,除了资源。
当一个对象被序列化时,PHP 将尝试对它调用__sleep()方法,当它被非序列化时,调用__wakeup()函数。这些是神奇的方法,你可以在你的类中实现它们来改变 PHP 处理这些事件的方式。
序列化一个对象给出了可以存储在 PHP 中的任何值的字节流表示。资源无法序列化。PHP 中的字符串可以包含字节流,因此可以将序列化的对象放入其中。
该字符串将引用被序列化的对象的类,并将包含与之关联的所有变量。对对象外部任何内容的引用都不能被存储,并且将会丢失,但是对对象内部任何内容的循环引用将会保留。
当您取消序列化对象时,PHP 必须声明该类。如果没有定义类,它将无法创建正确类型的对象,而是创建一个没有方法的类型__PHP_Incomplete_Class_Name的对象。
这是一个简单的例子,我们序列化和反序列化一个对象。
<?php
$objectOriginal = new A;
$string = serialize($objectOriginal);
file_put_contents('serialize.txt', $string);
// in another PHP file
$string = file_get_contents('serialize.txt');
$objectCopy = unserialize($string);
围绕序列化对象有很多潜在的安全问题。我们将在关于安全性的第六章中讨论它们,但是当你考虑非序列化的第二个(可选)参数时,记住这一点是值得的。此参数有助于减轻攻击,对手可以更改传递给unserialize(). 3 的参数值
unserialize 的第二个参数让您指定 PHP 应该愿意取消序列化的内容。始终使用它是安全的最佳做法。
| 价值 | 意义 |
|---|---|
| 省略 | PHP 可以实例化任何类的对象 |
FALSE | 不接受任何课程 |
TRUE | 接受所有课程 |
| 类名数组 | 仅接受指定的类别 |
| 任何其他值 | Unserialize()将返回 false 并发出一个E_WARNING |
下面是一个更全面的例子,说明如何在 PHP 中取消对象的序列化:
<?php
class A {
public function __wakeup() {
echo "Good morning";
}
};
class B {}
$a = new A();
$stored = serialize($a);
unset($a);
// this works because the class name is allowed
$a = unserialize($stored, ['allowed_classes' => [A::class]]);
// this creates __PHP_Incomplete_Class because the class doesn't match
$b = unserialize($stored, ['allowed_classes' => [B::class]]);
// this creates __PHP_Incomplete_Class because no classes are allowed
$c = unserialize($stored, ['allowed_classes' => false]);
// this works because all classes are allowed
$d = unserialize($stored, ['allowed_classes' => true]);
// this generates a warning because the parameter type is incorrect
$e = unserialize($stored, ['allowed_classes' => 'Not boolean or array']);
Caution
不要使用serialize()向用户传递数据。宁可用json_encode!为什么不呢?因为“所有用户输入都是潜在邪恶的”这句口头禅。你不想让用户有机会通过unserialize()运行他们的代码。
数组和对象之间的转换
我们在 PHP 基础知识一章中讨论了变量的造型。我们应该注意,也可以使用相同的语法在数组和对象之间进行转换。让我们看看:
<?php
$array = [
'key' => 'value',
'nested_array' => [
'another_key' => 'different_value'
]
];
$object = (object)$array;
var_dump($object);
在这个例子中,我使用了(object)转换语法来强制数组成为一个对象。PHP 将产生一个对象StdClass,它具有与数组的键相对应的属性。此代码输出:
object(stdClass)#1 (2) {
["key"]=>
string(5) "value"
["nested_array"]=>
array(1) {
["another_key"]=>
string(15) "different_value"
}
}
Note
嵌套数组不会转换为嵌套对象。
可以使用(array)转换语法将一个对象转换成一个数组。如果我们在代码清单的末尾运行命令assert((array)$object === $array);,代码将会无错地完成,因为断言通过了。
将对象转换为字符串
您可以通过声明__toString()方法来定义如何将对象转换为字符串。当 PHP 试图将你的对象转换成字符串时,它会调用这个方法并返回结果。
<?php
class User
{
private $firstName = 'Example';
private $lastName = 'User';
function __toString() {
return $this->firstName;
}
}
$user = new User;
// 'echo' expects a string type so PHP will implicitly cast the object to string
echo $user; // Example
这使您可以构建和格式化对您的对象有意义的字符串。如果您没有在对象上声明这个方法,那么 PHP 将生成一个可捕捉的致命错误,告诉您它不能将对象转换为字符串。
类别别名
PHP 允许您使用class_alias()函数为类创建别名。该函数接受三个参数——原始类名、为其创建的别名以及一个可选的布尔值,该值指示如果找不到类,是否必须调用自动加载程序。
乍看之下,可能无法立即看出类别名的用例是什么。它们的主要用例是有条件地导入名称空间。
use关键字是在编译时而不是运行时处理的。这意味着不可能使用条件逻辑来更改要导入的名称空间。class_alias()函数允许您有条件地导入名称空间。
例如,您可能希望根据 memcached 扩展是否可用来交换使用哪个类来缓存数据库。在下面的代码中,我们不能用关键字use导入替代类,但是通过使用类别名,我们可以改变cache引用的类。
<?php
if (extension_loaded('memcached')) {
class_alias('Memcached', 'Cache');
} else {
class_alias('InternalCacheProvider', 'Cache');
}
class Database
{
// The cache class is aliased to either Memcached or the InternalCacheProvider
public function __construct(Cache $cache) {}
}
构造函数和析构函数
构造函数是从类实例化对象时运行的方法。类似地,当对象被卸载时,会产生一个析构函数。
它们的声明如下例所示:
<?php
class constructorExample
{
// called when instantiated
public function __construct() {
}
// called when unloaded
public function __destruct() {
}
// PHP4 style constructor - deprecated in PHP7
public function constructorExample() {
}
}
构造函数优先级
在 PHP 4 中,构造函数方法通过与定义它们的类同名来识别。这种形式的构造函数在 PHP 7 中已被否决。
<?php
class constructorExample
{
// PHP4 style constructor - deprecated in PHP7
public function constructorExample() {
echo "Constructed!";
}
}
$test = new constructorExample;
为了向后兼容,如果找不到__construct()函数,PHP 7 将搜索与类同名的函数。该功能在 PHP 7 中已被否决,并将在未来的 PHP 版本中被删除。
如果我们从这个类构造一个对象,我们不会收到弃用警告。为什么不呢?PHP 7.1 首先寻找一个现代风格的构造函数,如果它存在,它将调用它。如果没有现代的构造函数,PHP 7.1 将寻找一个被否决的构造函数,如果它存在,它将生成一个警告并调用它。
构造函数参数
如果一个类构造函数带有一个参数,那么在实例化该类的一个实例时,您需要传递它。
<?php
class User {
public function __construct($name) {
$this->name = $name;
}
}
$user = new User('Alice');
这里我们将字符串"Alice"传递给构造函数。这方面的一个实际例子是依赖注入。4
遗产
PHP 在其对象模型中支持继承。如果你扩展了一个类,那么子类将继承父类的所有非私有属性和方法。换句话说,子类将拥有父类的公共和受保护元素。您可以在子类中重写它们,但是它们将具有相同的功能。
PHP 不支持一次从多个类继承。
导致类继承的语法非常简单。在声明类时,我们只需指出它所扩展的类的名称,如下例所示:
<?php
class ParentClass
{
public function sayHello() {
echo __CLASS__;
}
}
class ChildClass extends ParentClass
{
// nothing in this class
}
$kid = new ChildClass;
$kid->sayHello(); // ParentClass
在这个例子中,ChildClass被声明为扩展了ParentClass。它继承了sayHello()方法。
如果我们要定义一个继承自ChildClass的GrandChildClass,那么它也将继承所有的ParentClass方法。事实上,继承链中的任何类都将继承其祖先的所有方法和属性。
Note
神奇常数__CLASS__给出了当前正在执行的类的名称。我们在子类中调用继承的方法,但是它在父类中执行函数,因此报告类名为ParentClass。
最后一个关键字
PHP 5 引入了final关键字。您可以将它应用于整个类,也可以应用于类中的特定方法。关键字final的作用是防止类被扩展或者方法被覆盖。所有最终属性和方法的可见性都是公共的。
将类或函数标记为 final 有助于避免在扩展类时错误地更改行为。
如果您试图覆盖子类中的 final 方法,或者如果您试图声明一个扩展了标记为 final 的类的类,PHP 将发出致命错误。
您可以通过在类或方法的定义前使用final关键字将类或方法标记为 final,就像下面这个例子,我将函数标记为 final:
class Employee
{
final public function calculateWage(float $hourlyRate, int $numHoursWorked)
{
return $hourlyRate * $numHoursWorked;
}
}
让我们看另一个例子,它显示了产生的错误并突出了关键字的有用性。
以下示例中的代码清单没有使用任何关键字final,因此运行时不会出错,并为雇员计算出一份相当丰厚的工资。我已经注释了两行,分别显示了当我们将类或方法标记为 final 时将会抛出的错误。
<?php
// Fatal error: Class Oops may not inherit from final class (Employee)
final class Employee
{
\final public function calculateWage(float $hourlyRate, int $numHoursWorked)
{
return $hourlyRate * $numHoursWorked;
}
}
// Fatal error: Class CannotExtendFinalClass may not inherit from final class (Employee)
class Oops extends Employee {
// Fatal error: Cannot override final method Employee::calculateWage() in /in/afkAJ on line 17
public function calculateWage(float $hourlyRate, int $numHoursWorked) {
if ($this->employeeName === 'Andrew') {
return 1000000;
}
return $hourlyRate * $numHoursWorked;
}
}
$oops = new Oops;
$oops->employeeName = 'Andrew';
echo $oops->calculateWage(10.00, 50);
Note
这与 Java 中 final 的使用有些不同,Java final 关键字的 PHP 等价物是const。
最重要的
子类可以声明一个与父类同名的方法,前提是该方法在父类中没有被标记为 final。
子级中的方法参数签名必须与父级相似;例如,下面的代码将生成一条警告,指出子声明需要与父声明兼容:
<?php
class Employee
{
public function calculateWage(float $hourlyRate, int $numHoursWorked)
{
return $hourlyRate * $numHoursWorked;
}
}
class Oops extends Employee {
public function calculateWage(int $hourlyRate, int $numHoursWorked) {
return $hourlyRate * $numHoursWorked;
}
}
如果一个函数像这样被覆盖并在子类上被调用,那么父类将不会被调用。
这适用于构造函数和析构函数,但在这些情况下,通常是这样解决的:
<?php
class ChildClass extends ParentClass
{
public function __construct() {
parent::__construct();
// more constructor functions here
}
}
对parent::__construct()的调用将调用父类的构造函数方法。当控制流返回到子节点时,将调用其构造函数中的其余函数。
如果子类覆盖了父类的方法,那么子类的可见性不能低于父类。
换句话说,如果父方法是公共的,那么子方法就不能重写为受保护的或私有的方法。
接口
接口允许您指定一个类必须实现什么方法,而无需指定实现的细节。
它们通常用于在面向服务的架构范例中定义契约,但是也可以在您想要规定未来的类如何与您的代码交互时使用。
接口中的所有方法都必须声明为公共的,并且它们本身不能有任何实现。
接口不能有属性,但是可以有常量。
接口的声明如下所示:
<?php
interface PaymentProvider
{
public function showPaymentPage();
public function contactGateway(array $messageParameters);
public function notify(string $email);
}
一个类将被声明为实现它,如下所示:
<?php
class CreditCard implements PaymentProvider
{
public function showPaymentPage() {
// implementation
}
public function contactGateway() {
// implementation
}
public function notify(string $email) {
// implementation
}
}
通过列出用逗号分隔的接口名称,类可以一次实现多个接口。
类可能只从一个类继承,但可能实现许多接口。
抽象类
PHP 支持抽象类,即包含一个或多个抽象方法的类。abstract方法是已经声明但还没有实现的方法。
在下面这个abstract类的例子中,函数girlDescendingStairs()是一个抽象方法。它是使用abstract关键字定义的,没有任何实现。注意这里没有用于abstract方法的代码块。
<?php
abstract class Paintings
{
abstract protected function girlDescendingStairs();
protected function persistenceOfMemory() {
echo " I have an implementation so this is not an abstract method ";
}
public function __construct() {
echo "I cannot be constructed!";
}
}
无法构造一个abstract类;我们不能从类Paintings创建新对象。
抽象类是用来扩展的。扩展abstract类的类必须定义父类中所有标记为抽象的方法。如果子类不实现这些方法,它们也必须被标记为抽象,因此子类也将是抽象的。
当一个子类扩展一个abstract类时,它必须定义具有相同或更少限制的可见性的抽象方法。
子类中声明的方法的签名必须与抽象方法的签名匹配。这意味着该方法的必需(非可选)参数的数量和类型必须相同。
私有方法不能标记为抽象。让我们看看如何扩展抽象类:
<?php
abstract class Paintings
{
abstract protected function girlDescendingStairs();
protected function persistenceOfMemory() {
echo "I have an implementation so this is not an abstract method";
}
public function __construct() {
echo "I am being constructed!";
}
}
class Foo extends Paintings {
public function girlDescendingStairs() { echo "Whee!"; }
}
$foo = new Foo; // I cannot be constructed!
$foo->girlDescendingStairs(); // Whee!
我定义了一个新的类,我把它想象成Foo,它扩展了抽象类。我已经实现了抽象方法girlDescendingStairs,并将可见性从protected更改为限制较少的范围public。我还没有覆盖抽象类定义的非抽象方法。
Foo类没有抽象方法,所以我可以从它构造一个对象。注意,当我这样做时,父类的构造函数被调用,因此Foo错误地报告它不能被构造。
匿名类
PHP 7 引入了匿名类,允许你动态定义一个类,并从中实例化一个对象。下面是一个使用匿名类的简单例子:
<?php
$object = new class('argument') {
public function __construct(string $message) {
echo $message;
}
};
请注意我们是如何内联定义类的,并且可以使用与从命名类创建对象类似的语法来传递参数。这段代码将输出字符串"argument",因为调用了构造函数,并将字符串“argument”传递给了它。
匿名对象的一个用例是扩展一个命名类;例如,如果要重写方法或属性。不必在单独的文件中声明一个类,您可以创建一个一次性的内联实现。
反射
PHP 反射 API 允许您在运行时检查 PHP 元素并检索关于它们的信息。
反射 API 是在 PHP 5.0 中引入的,因为 PHP 5.3 在默认情况下是启用的。
使用反射的一个常见地方是在单元测试中。反射有用的一个例子是测试类中私有属性的值。您可以使用反射使私有属性可访问,然后进行断言。
有几个反射类允许您检查特定类型的变量。这些类中的每一个都是根据您可以用来检查的变量类型来命名的。
| 班级 | 用于检查 |
|---|---|
ReflectionClass | 班级 |
ReflectionObject | 目标 |
ReflectionMethod | 对象的方法 |
ReflectionFunction | 像 PHP 核心函数或用户函数这样的函数 |
ReflectionProperty | 性能 |
PHP 手册 5 有关于反射类及其方法的详尽文档。
我们简单看一个使用ReflectionClass的例子。
<?php
$reflectionObject = new ReflectionClass('Exception');
print_r($reflectionObject->getMethods());
传递给反射类的构造函数的参数要么是类的字符串名称,要么是类的具体实例(对象)。
ReflectionClass对象有几个方法允许您检索关于被检查类的信息。在前面的例子中,我们输出了一个包含了Exception类所有方法的数组。
类型提示
类型提示允许您指定函数的参数应该是什么类型的变量。
在下面的例子中,我们指定传递给函数printArray()的参数$arr必须是一个数组。
<?php
function printArray(array $arr) {
echo "<pre>" . print_r($arr,true) . "</pre>";
}
// The parameter to the function must be a class that implements the PaymentProvider interface
function sendNotificationToPaymentProvider(PaymentProvider $paymentProvider)
{
$paymentProvider->contactGateway($messageParameters);
}
function sayHello(string $name)
{
echo "Hello " . $name;
}
在 PHP 5 中,如果你传递了一个错误类型的参数,那么就会产生一个可恢复的致命错误。在 PHP 7 中,抛出了一个TypeError异常。
从 PHP 7 开始,类型提示被称为“类型声明”。我将使用这个新的命名法,但是这些术语在 PHP 的上下文中是可以互换的。
您可以将复合类型、可调用类型和标量变量类型指定为类型提示。此外,如果将NULL用作函数的默认参数,则可以使用NULL类型提示。
<?php
function nullExample(null $msg = null) {
echo $msg;
}
如果您指定一个类作为类型提示,那么它的所有子类和实现都将是有效的参数。
<?php
class A {}
class B extends A {}
class C extends B {}
function foo(A $object) {}
$testObj = new C;
foo($testObj); // no error produced
在这个例子中,我们的函数需要一个 A 类的对象。我们正在传递一个 B 类的对象。因为 B 继承自 A,所以我们的代码将会运行。
如果您提供的类名是一个接口,那么 PHP 将允许任何实现该接口的对象(或者是实现该接口的类的祖先)通过。
类别常数
常数是不可变的值。
类常量允许您在每个类的基础上定义这样的值;它们不会在类别的执行个体之间变更。从该类创建的所有对象都具有相同的类常量值。
类常量遵循与变量相同的命名规则,但是没有前缀符号$。按照惯例,常量名称以大写形式声明。
让我们考虑一个例子:
<?php
class MathsHelper
{
const PI = 4;
public function squareTheCircle($radius) {
return $radius * (self::PI ** 2);
}
}
echo MathsHelper::PI; // 4
类常量是公共的,因此可以从所有范围访问。当我们从类外部访问它时,我们使用范围解析操作符和声明它的类名。
该值必须是常量表达式,而不是(例如)变量、属性或函数调用。
与传统常量一样,类常量可能只包含标量值。
后期静态绑定
后期静态绑定是在 PHP 5.3.0 中引入的,是一种在静态继承的上下文中引用被调用类(相对于调用类)的方法。
这个想法是引入一个关键字来引用运行时最初调用的类,而不是定义该方法的类。
我们决定使用关键字static,而不是引入一个新的保留字。
转移呼叫
“转移”呼叫是由parent::、static::引入的静态呼叫或由功能forward_static_call()调用的呼叫。
如果类因为没有定义方法而退回到继承的类,对self::的调用也可以是转发调用。
后期静态绑定的工作原理是将类存储在最后一个“非转发调用”中。换句话说,后期静态绑定解析将在完全解析的静态调用处停止。
我将详细介绍一个 PHP 手册示例的修改示例。
<?php
class A {
public static function foo() {
echo static::who();
}
public static function who() {
return 'A';
}
}
class B extends A {
public static function test() {
A::foo();
parent::foo();
self::foo();
}
}
class C extends B {
public static function who() {
echo 'C';
}
}
C::test(); // ACC
ACC 的输出一开始可能与直觉相反,但是让我们慢慢来。
对C::test()的呼叫被完全解决,因此 C 类最初被存储为最后一个非转移呼叫。
函数 C 中没有test()方法,所以调用被隐式转发给它的父类。所以类 B 中的test()方法被调用。
对 A::foo()的调用
test()中的第一个调用专门将类 A 命名为作用域。这意味着呼叫已完全解决。被存储为最后一个非转移呼叫的类被更改为。
调用 A 中的foo()方法,并解析static关键字,以找到对哪个类调用who()方法。
最后一个非转发调用是对 A 中的一个类的,因此调用了 A 中的who()方法。
对 parent::foo()的调用
test()中的下一个调用引用了 B 的父类,因此该调用被显式转发给 B 的父类,即 a。
这是一个转发的调用,因此存储为最后一个完全解析的静态调用(即 C)的值保持不变。
调用 A 中的foo()方法,并解析static关键字,以找到对哪个类调用who()方法。
最后一个非转发调用是对 C 中的一个类的,因此调用了 C 中的who()方法。
调用 self::foo()
类 B 没有定义foo()方法,因此调用被隐式传递给父类 a。
这是一个转发的调用,因此作为最后一个完全解析的静态调用(即 C)存储的值保持不变。
这导致在类 a 中解析静态关键字时调用类 C 的who()方法。
魔术(__*)方法
PHP 将任何名称以两个下划线为前缀的方法视为神奇的方法。PHP 在对象生命周期的特定时间“神奇地”调用这些方法(无需您调用它们)。我喜欢把它们想象成类似于在事件中被调用的钩子。当对象发生相关事件时,PHP 调用 magic 方法。
PHP 并没有为这个类提供一个实现,在你的类中重写这个方法是由你作为程序员来决定的。
魔法方法只适用于类;它们不是独立的功能。
有 15 个预定义的神奇函数,建议避免用双下划线前缀命名其他函数。
__get 和 __set
当 PHP 试图读取(获取)或写入(设置)不可访问的属性时,就会调用这些神奇的方法。
<?php
class BankBalance {
private $balance;
public function __get($propertyName) {
// echo "No property " . $propertyName;
return "No value";
}
public function __set($propertyName, $value) {
echo "Cannot set $propertyName to $value";
}
}
$myAccount = new BankBalance();
$myAccount->balance = 100;
// Cannot set balance to 100No value
echo $myAccount->nonExistingProperty;
向__get()方法传递了正在查找的属性的名称。您可以为方法中缺少的属性返回值,或者按照您喜欢的方式处理它。
在这个例子中,被注释的代码可以被替换为逻辑来处理丢失的属性,不存在的属性将被设置为No value。
一个附加参数$value被传递给__set()。
_ _ 已设置和 _ _ 未设置
通过在不可访问的属性上调用isset()函数或empty()来触发__isset()方法。
通过在不可访问的属性上调用unset()函数来触发__unset()方法。
两种方法都接受一个字符串参数,该参数包含作为参数传递给函数的属性的名称。
您可以使用这些神奇的方法让isset()、empty()和unset()函数处理私有的和受保护的属性。
__call 和 __callStatic
如果您试图在一个对象上调用一个不存在的方法,就会调用这些神奇的方法。唯一不同的是__callStatic()响应静态调用而__call()响应非静态调用。
<?php
class Politician {
public function __call($method, $arguments) {
echo __CLASS__ . ' has no ' . $method . ' method';
}
}
$jacob = new Politician();
$jacob->honesty(); // Politician has no honesty method
在这两种情况下,都会向 magic 方法传递一个字符串,该字符串包含调用试图查找的方法的名称,以及一个传递的参数数组。
_ _ 调用
当你试图将一个对象作为一个函数来执行时,这个神奇的方法__invoke()就会被调用。
<?php
class Square
{
public function __invoke($var) {
return $var ** 2;
}
}
$callableObject = new Square;
echo $callableObject(10); // 100
Caution
这种语法可能会与变量函数名混淆,所以要小心。
_ _ _ debug info
这个神奇的方法由var_dump()在转储对象时调用,以确定应该输出哪些属性。
默认情况下,var_dump()将输出对象的所有公共、受保护和私有属性。
<?php
class Dictatorship {
private $wmd = 'Nuke';
public $oil = 'Lots';
// we are going to hide our wmd
public function __debugInfo() {
return [
'oil' => $this->oil
];
}
}
$country = new Dictatorship();
var_dump($country);
/*
object(Dictatorship)#1 (1) {
["oil"]=>
string(4) "Lots"
}
*/
这个例子将防止$wmd变量包含在var_dump()中。
更多神奇功能
我们已经在“构造函数和析构函数”一节中讨论了__construct()和__destruct()函数。
我们已经在“序列化对象”一节中讨论了__sleep()和__wake()。
我们在讨论“复制对象”时查看了__clone(),在“将对象转换为字符串”一节中查看了__toString()。
标准 PHP 库(SPL)
标准 PHP 库是类和接口的集合,是解决常见编程问题的方法。从版本 5.0.0 开始,它就可用 PHP 编译。
这些课分成几类。关于类的完整列表,请参考 SPL 的 PHP 手册。 6
| 种类 | 用于 |
|---|---|
| 数据结构 | 标准数据结构,如链表、双向链表、队列、栈、堆等。 |
| 迭代程序 | |
| 例外 | |
| 文件处理 | |
ArrayObject | 用数组函数访问对象。 |
SplObserver和SplSubject | 实现观察者模式。 |
SPL 还提供了几个功能。它们大多属于广泛的反射和自动加载类别。
数据结构
第一类函数是数据结构。如果你已经熟悉了数据结构,你会很高兴知道 SPL 实现了多种数据结构。这些包括双向链表、堆、数组和映射。
数据结构在编程算法中非常有用。
迭代程序
迭代器允许你遍历对象和集合。迭代器维护一个指向元素的游标。
PHP 迭代器将允许你在容器的所有元素中前进或后退光标。它们还允许您执行其他操作,例如,ArrayIterator将允许您对数组执行排序。
如果没有 PHP 提供的类,您将需要自己实现这些迭代器,但幸运的是,所有这些艰苦的工作都由善良的 PHP 作者完成了。
迭代器的列表相当广泛。我不认为你需要列出它们,但是你应该知道它们是 SPL 的一部分。它们将至少提供光标移动能力和一些可能的额外功能。
例外
SPL 也包括标准的Exception类。抛出特定于已发生的错误类型的异常是一种好的做法。这使得编写正确处理异常的catch块变得更加容易。
SPL 引入了一些异常类,这使得抛出特定的异常更加方便。
SPL 异常分为两类——逻辑异常和运行时异常。这些类别中的每一个都有许多异常类,这些异常类关注可能发生的特定种类的错误。
如果它们出现在问题中,你至少应该能够认出它们。
逻辑异常
LogicException(延长Exception)BadFunctionCallExceptionBadMethodCallExceptionDomainExceptionInvalidArgumentExceptionLengthExceptionOutOfRangeExceptionRuntime例外情况RuntimeException(延长Exception)OutOfBoundsExceptionOverflowExceptionRangeExceptionUnderflowExceptionUnexpectedValueException
文件处理
SPL 也提供帮助处理文件的课程。
SplFileInfo类为单个文件的信息提供了一个高级面向对象的接口。它提供了一些方法,您可以使用这些方法来查找文件的名称、大小、权限和其他属性。您还可以判断该文件是否是一个目录,是否是可执行的,以及许多其他功能。
SplFileObject类为文件提供了一个面向对象的接口。您可以使用它来打开和读取文件。在处理文件时,有前进或后退文件、查找特定位置的方法以及其他有用的功能。
SplTempFileObject类为临时文件提供了一个面向对象的接口。您可以像使用任何其他输出文件一样使用该文件,但是它会在脚本完成后被删除。例如,您可以在图像处理或验证文件上传时使用它。
数组对象
SPL 还包括各种各样的类和接口。第一个是ArrayObject,允许对象作为数组工作。
当你构造一个ArrayObject时,你可以传递一个数组作为它的参数。最终的对象将具有模拟 PHP 数组函数的方法。
ArrayObject有相当多的限制,但是它的优势之一是你可以定义自己的迭代方式。
观察者模式
最后,让我们看看 SPL 中包含的两个接口— SplObserver和SplSubject。注意,这些是接口而不是类,所以您需要实现实际的行为。
这两个接口一起实现了观察者模式。
观察者模式是一种软件设计模式,在这种模式中,一个名为 subject 的对象维护一个名为 observer 的依赖者列表,并自动通知它们任何状态变化,通常是通过调用它们的方法之一。这种模式主要用于实现分布式事件处理系统。
使用这些接口将使您的代码更具可移植性,因为其他库将能够与您的主题和观察者进行交互。
发电机
生成器为您提供了一种创建迭代器对象的简单方法。
使用迭代器和生成器的好处是,您可以构建一个对象,无需计算整个数据集就可以遍历它。这节省了处理时间和内存。
用例可能是替换通常返回数组的函数。该函数将计算所有的值,分配一个数组变量来存储它们,并返回数组。
生成器只计算和存储一个值,并将其输出给迭代器。当迭代器需要下一个值时,它调用生成器。当生成器用完所有值时,它可以退出或者返回一个最终值。
生成器可以像任何迭代器一样被迭代,如下例所示:
<?php
function generator() {
for ($i = 0; $i < 99; $i++) {
yield $i;
}
}
foreach (generator() as $value) {
echo $value . " ";
}
Yield 关键字
关键字yield类似于一个函数 return,除了它用于在暂停生成器执行时将一个值返回给迭代器。
生成器的范围在调用之间保持不变。在生成器让步后,变量不会失去它们的值。
<?php
function exampleGenerator() {
// some functions
$data = yield $value;
}
用钥匙屈服
使用生成器可以生成作为函数关联数组的键值对。
如果没有显式地使用键,那么 PHP 将把产生的值与递增的顺序键配对,就像对枚举数组一样。
生成键值对的语法类似于声明关联数组:
<?php
function myGenerator() {
// some functions
yield $key => $value;
}
产生空值
不带参数调用 yield 会导致它产生一个带有自动递增顺序键的NULL值。
通过引用产生
生成器函数可以通过引用产生变量,这样做的语法是在函数名前面加上一个&符号。
<?php
function &referenceGenerator() {
// some functions
yield $value;
}
从发电机返回
在您的生成器完成处理后,您可以从中返回一个值。这使得生成器的最终值更加明确。
<?php
function sowCrops() { return 'wheat'; }
function millWheat() { return 'flour'; }
function bake($flour) { return 'cupcake'; }
function generator() {
$wheat = yield sowCrops();
$flour = yield millWheat();
return bake($flour);
};
$gen = generator();
foreach ($gen as $key => $value) {
echo $key . ' => ' . $value . PHP_EOL;
}
echo $gen->getReturn();
/*
0 => wheat
1 => flour
cupcake
*/
这个语法清楚地表明了生成器的返回值是什么。如果没有它,您将需要假设最后产生的值是返回值。
发电机委托
生成器委托让您将处理值的责任委托给另一个可遍历的对象或数组。
这样做的语法是yield from <expression>:
<?php
function generator() {
$a = [1,2,3];
yield from $a;
yield from range(4,6);
yield from sevenAteNine();
}
function sevenAteNine() {
for ($i=7; $i<10;$i++) {
yield $i;
}
}
$gen = generator();
foreach ($gen as $value) {
echo $value . PHP_EOL;
}
在这个例子中,我们使用三种方式将生成委托给另一个可遍历的对象或数组。
运行这段代码的结果是从 1 数到 9。
特征
特性是在 PHP 5.4.0 中引入的,旨在减轻单一继承语言的一些限制。
Note
性状不满足真正遗传的“是-是”关系。如果你熟悉其他语言的 mixins,它们更类似于那些。
trait 包含一组方法和属性,就像一个类一样,但是不能自己实例化。相反,trait 包含在一个类中,然后该类可以使用它的方法和属性,就好像它们是在类本身中声明的一样。
换句话说,特征被放在一个类中,无论方法是在特征中定义的还是在使用该特征的类中定义的,都没有关系。您可以将 trait 中的代码复制并粘贴到类中,并以同样的方式使用它。
trait 中包含的代码旨在封装可重用的属性和方法,这些属性和方法可以应用于多个类。
声明和使用特征
我们使用trait关键字来声明一个特征;为了将它包含在一个类中,我们使用了use关键字。一个类可以使用多个特征。
<?php
trait Singleton
{
private static $instance;
public static function getInstance() {
if (!(self::$instance instanceof self)) {
self::$instance = new self;
}
return self::$instance;
}
}
class UsingTraitExample
{
use Singleton;
}
$object = UsingTraitExample::getInstance();
var_dump($object instanceof UsingTraitExample); // true
在这个例子中,我们声明了一个 trait,它包括实现 singleton 模式所需的方法和属性。
当我们想让一个新类遵循单例模式时,我们可以通过使用 trait 来实现。我们不必在类中实现该模式,也不必在继承层次结构中包含该模式。
命名空间特征
如果特征有冲突的名字,PHP 将产生一个致命的错误,但是特征可以在名称空间中定义。
如果您试图在一个不在同一个名称空间层次结构中的类中使用 trait,那么您将需要在包含它时指定完全限定名。
继承和优先
特征可能不会扩展其他特征或类,但是您可以简单地在另一个特征中使用一个特征。
使用特征在类中声明的方法优先于在特征中声明的方法。然而,trait 中的方法将覆盖由类继承的方法。
更简单地说,性状和类别的优先顺序如下:
类成员>特征方法>继承方法
冲突解决
如果两个特征试图插入一个同名的方法,PHP 将产生一个致命错误,除非你明确地解决这个冲突。
PHP 允许您使用insteadof操作符来指定您希望它使用的冲突方法。
这允许您排除一个 trait 方法,但是如果您想保留两个方法,您需要使用as操作符。as操作符允许您包含一个冲突的方法,但是使用不同的名称来引用它。
下面是一个很长的例子来说明这种用法:
<?php
trait Dog {
public function makeNoise() {
echo "Woof";
}
public function wantWalkies() {
echo "Yes please!";
}
}
trait Cat {
public function makeNoise() {
echo "Purr";
}
public function wantWalkies() {
echo "No thanks!";
}
}
class DomesticPet
{
use Dog, Cat {
Cat::makeNoise insteadof Dog;
Cat::wantWalkies as kittyWalk;
Dog::wantWalkies insteadof Cat;
}
}
$obj = new DomesticPet();
$obj->makeNoise(); // Purr
$obj->wantWalkies(); // Yes please!
$obj->kittyWalk(); // No thanks!
Note
单独使用as是不够的。你仍然需要使用insteadof来排除你不想用的方法,然后你只能使用as来做一个新的方法来引用旧的方法。
能见度
您可以通过扩展关键字use将可见性修饰符应用于函数,如下例所示:
<?php
trait Example {
public function myFunction() {
// do stuff
}
}
class VisbilityExample {
use Example {
myFunction as protected;
}
}
$obj = new VisbilityExample();
$obj->myFunction(); // PHP Fatal error: Call to protected method
我们指定该方法应该在类中被保护,即使它在 trait 中被声明为 public。您可以在块中包含多个函数,每个函数都有自己的可见性。
Chapter 5 Quiz
Q1:以下哪一个不是有效的 PHP 类名?
| exampleClass |
| Example_Class |
| Example_1_Class |
| 1_Example_Class |
| 它们都是有效的类名 |
Q2:这段代码运行后,属性$name将包含什么?
| Dozy |
| Asleep |
| Rested |
| 这段代码不会运行 |
<?php
class SleepyHead {
protected $name = "Dozy";
public function __serialize() {
$this->name = "Asleep";
}
public function __unserialize() {
$this->name = "Rested";
}
}
$obj = unserialize(serialize(new SleepyHead()));
Q3:为了让脚本输出"Castor",我们可以用下面哪个语句替换注释行?
| $twin = $star; |
| $twin = clone($star); |
| $twin &= $star; |
| $twin = new clone($star); |
<?php
$star = new StdClass;
// replace this line
$star->name = "Castor";
$twin->name = "Pollux";
echo $star->name; // must be Castor
Q4:假设对象 A 有一个属性是对象 B 的实例,如果我们克隆了对象 A,那么 PHP 是否也会克隆它的属性之一 B?
| 是 | | 不 | | 您不能克隆包含对其他对象的引用的对象 |
Q5:不能用相同的名字声明两个函数。选择尽可能多的适用项。
| 真实的 | | 假的;您可以在不同的名称空间中声明它们 | | 假的;您可以在它们的构造函数中声明不同数量的参数,PHP 将选择与您的实例化相匹配的定义 | | 假的;您可以在不同的范围内声明它们 |
Q6:当你对一个对象调用json_encode函数进行序列化时,PHP 会调用哪个神奇的方法?
| __sleep |
| __wake |
| __get |
| __clone |
| 这些都不是 |
对或错:接口只能指定公共方法,但是你的类可以按照你喜欢的方式实现它们。
| 真实的 | | 假的;接口可以指定任何可见性 | | 假的;您根本无法在实现时更改可见性 | | 假的;您只能将可见性更改为不太可见的可见性 |
Q8:这段代码的输出会是什么?
| Hello World |
| I have the world |
| 错误 |
| 以上都不是 |
<?php
class World {
public static function hello() {
echo "Hello " . __CLASS__;
}
}
class Meek extends World {
public function __call($method, $arguments) {
echo "I have the world";
}
}
Meek::hello();
问题 9:在特征、类和继承方法中声明的函数的优先级是以下哪一个?
| 继承方法➤特征方法➤类成员 | | 类成员➤特质方法➤继承方法 | | 类成员➤特质方法➤继承方法 | | 特征方法➤类成员➤继承的方法 |
问题 10:对或错:受保护的方法不能调用私有方法,即使它们在同一个类中。
| 真实的 | | 错误的 |
Footnotes 1
http://www.php-fig.org/psr/psr-4/examples/
2
https://php.net/manual/en/function.spl-autoload-register.php
3
https://www.owasp.org/index.php/PHP_Object_Injection
4
https://en.wikipedia.org/wiki/Dependency_injection
5
https://php.net/manual/en/class.reflectionclass.php
6
https://php.net/manual/en/book.spl.php
六、安全
安全性是 web 应用的主要关注点。甚至像联合国这样的主要组织也曾被黑客利用非常简单的安全漏洞攻击过。
我认为没有完全安全的系统。我保护应用的目的有两个。首先,我的目标是让攻击者获得访问权限的时间尽可能长。我的下一个目标是把他们能找到的任何信息的价值降到最低。换句话说,我从不认为我的系统是不可渗透的,我总是使用深度防御。
这降低了黑客入侵我的应用的可行性——这将需要很长时间才能进入,当他们进入时,他们需要花费相当大的努力来获得任何有价值的信息。
当你被老虎追赶时,你不需要跑得比老虎快。你只需要比你旁边的小伙子跑得快。
Note
安全的主要缺陷之一是社会工程。关于社会工程的讨论不在 Zend 考试的范围之内,但是你必须记住,不仅仅是你的代码和服务器是你数据的入口点。
配置
配置 PHP 的最佳方法是确保您与最新版本保持同步,并使用它们带来的改进。
如果您没有使用 PHP 最新的稳定版本,而是使用旧版本,那么您应该有一个非常充分的理由。
确保你的操作系统打了补丁。定期应用安全更新,并确保及时了解安全新闻。
只有在有机会确保其他包更新不会对您的堆栈或测试环境产生负面影响时,您才应该应用它们。很可能你的发行版库管理员会小心不去破解常用的栈,但是如果你使用了一个不常用的栈或者从你的库之外安装了软件,那么升级的时候就要小心了。
错误和警告
您应该将 PHP 配置为在生产过程中隐藏警告和错误。错误和警告可以让人了解代码的内部工作方式,比如目录名和正在使用的库。这类信息可以帮助他们利用您的堆栈中的漏洞。
您可以在您的php.ini文件中或者在运行时用error_reporting()函数设置错误报告。两者都有一个数字参数,通常是根据预定义的错误常数构建的表达式形式。
这些是推荐的生产设置,以及 PHP 7.1 默认的 1 生产设置:
| 环境 | 价值 |
|---|---|
display_errors | Off |
log_errors | On |
error_reporting | E_ALL & ∼E_DEPRECATED & ∼E_STRICT |
这些也是你可以假设在你的 Zend 考试中设置的设置,当然,除非问题另有说明。
在开发中,您的error_reporting设置应该是E_ALL,并且您的代码必须在没有警告的情况下运行——不要使用被否决的函数。
旗帜是如何工作的
您可能想知道这些标志是如何设置的,为什么我们要对它们使用按位运算符。我会试着解释一下,以便更容易理解你的配置设置。
将二进制格式的数字想象成一系列 1 和 0。二进制数中的每个位置都是一个与选项相关联的标志。如果该位置的数字为 1,则标志打开,选项设置。
现在,E_ALL是一个被选择的数字。如果你var_dump(E_ALL),你得到输出int(32767),也就是0b111111111111111。
每个选项都是一个数字,被选择为具有一个且仅一个位设置。比如E_NOTICE是8,二进制是0b1000,E_DEPRECATED是8192,二进制是0b10000000000000。请注意,您可以根据需要在左侧填充多个 0,使其长度相同。
按位运算符∼翻转一个数中的位,所以∼E_NOTICE就是0b0111。
按位运算符&比较两个数的位位置。如果两个数在位置集合中都有一位,那么结果就是true。因此,E_ALL & ∼E_NOTICE已经设置了所有的位,除了那个表示E_NOTI CE 开启的位。
结果是您将error_reporting设置为一个数字,该数字具有为您想要打开的选项设置的位。
禁用函数和类
您可以在您的php.ini文件中使用disable_functions和disable_classes指令来防止函数和类被使用。这些设置只能在您的php.ini文件中设置,不能在运行时或在目录ini文件中更改。
需要禁用的常用函数包括允许 PHP 执行系统命令的函数:exec、passthru、shell_exec和system。
DirectoryIterator和Directory类通常也被禁用,因为它们可能被攻击者利用。
Hint
禁用这些功能是一种“黑名单”方法。有创造力的对手会把这看作一个障碍,而不是不可逾越的障碍。
PHP 作为 Apache 模块
如果 PHP 作为 Apache 模块运行,那么它将使用与 Apache 服务器相同的用户运行。这意味着它将拥有与 Apache 用户相同的权限和访问权。
最佳实践是为 Apache 设置一个用户,而不是作为“nobody”运行它。Apache 用户应该对文件系统具有有限的访问权限,并且不应该在sudoers列表中。
您应该使用 PHP open_basedir设置来限制 PHP 可以访问的目录。您可以将它与设置doc_root进行对比,后者影响 PHP 将从哪些目录提供文件。
Note
设置open_basedir不受安全模式影响,但doc_root受影响。
如果您保留一个目录,其中存储了由该目录之外的用户上传的文件,您可以使攻击者上传和执行文件变得更加困难。
PHP 作为 CGI 二进制文件
我不知道是否还有人将 PHP 作为 CGI 二进制文件运行,但是这个话题仍然在 Zend 的教学大纲中。我试图理解为什么 Zend 觉得理解他们很重要。
我认为在传统配置中理解这些问题的价值在于,在现代设置中也有类似的问题。例如,“向 PHP 传递不受控制的请求”的配置缺陷 2 似乎与 CGI 中绕过权限检查的技巧非常相似(稍后将会介绍)。
PHP-FPM 使用 FastCGI 协议运行,这是对 CGI 的改进。这一部分与 PHP-FPM 无关,因为请求是通过套接字传递给它的,不会受到 URL 的影响。
出于考试目的,您需要了解三个配置参数以及它们在 CGI 攻击环境中的作用。我在这里列出它们,然后更详细地解释它们。
| 环境 | 功能 |
|---|---|
cgi.force_redirect | 阻止 PHP 执行,除非 web 服务器调用它。如果设置为 on,那么 PHP 将不会响应类似 http://yoursite.com/cgi-bin/php/ ...的请求。 |
doc_root | 设置文档根目录。如果您将safe_mode设置为 on,那么 PHP 将不会为这个目录之外的文件提供服务。 |
user_dir | 为 web 用户设置主目录。 |
doc_root和user_dir设置并不仅仅与 CGI 安全相关,应该作为一般安全设置的一部分进行设置。
恶意 CGI 参数
通常 URL 中问号后面的查询信息通过 CGI 接口作为命令行参数传递给解释器。这适用于任何被 web 服务器用作 CGI 的二进制文件。
根据惯例,URL http://my.host/cgi-bin/php?/etc/passwd 会尝试将/etc/passwd传递给 PHP 二进制文件。通常,CGI 解释器打开并执行命令行中第一个参数指定的文件。
然而,当作为 CGI 二进制文件调用时,PHP 拒绝解释命令行参数。这使得它不会受到依赖于向二进制文件传递参数的攻击。
绕过权限检查
通常使用“友好的”URL,将搜索引擎友好的可读 URL 发送给脚本。例如,URL https://yourhost.com/user/nico.php 可能映射到对 https://yourhost.com/cgi-bin/php/user/belieber.php 的实际请求。
通常,web 服务器会检查权限,并验证访问者必须访问/user/目录。如果访问者被允许,它将创建重定向的请求。
如果访问者访问重定向的目标,即包含cgi-bin的完整 URL,那么 web 服务器将检查他们访问/cgi-bin/目录的权限,而不是将要提供的实际目录。
这意味着访问者只需在cgi-bin中从 PHP 请求文件,就可以绕过保护用户目录的权限检查。恶意访问者可以访问 web 服务器上 PHP 可以读取的任何文件。
cgi.force_redirect、doc_root和user_dir指令用于防止 PHP 访问私有文档。
cgi.force_redirect设置阻止了从 URL 直接调用 PHP 只有在从 Apache 等 web 服务器的重定向上调用 PHP 时,它才会执行。
当使用 PHP 作为 CGI 二进制文件时,您应该考虑将 PHP 二进制文件移出文档树,并将可执行 PHP 脚本与静态脚本分开。
运行 PHP-FPM
PHP-FPM 允许你很容易地设置多个池,每个池可以在不同的用户下运行。如果你有多个客户端,那么你应该确保每个客户端的网站都作为自己的用户运行。客户端用户不应该对其主目录之外的文件有任何访问权限。
以下是池文件中的一些设置示例:
[pool1]
user = site1
group = site1
listen = /var/run/php5-fpm-site1.sock
listen.owner = www-data
listen.group = www-data
我们将名为pool1的池设置为以组site1中的用户site1的身份运行。我们将监听所有者和组设置为 web 服务器用户,以便 Nginx/Apache 可以读写套接字。
一旦我们设置了池运行的用户,我们将配置文件权限以限制它只能访问网站所在的目录。这将防止客户使用文件读取功能来读取另一个客户的目录内容。
Tip
一些文件,比如 WordPress 站点中的wp-config.php,有可预测的名字,保护用户目录不被其他用户访问非常重要。
我们配置 Nginx 来传递 PHP 请求,如下所示:
location ∼ \.php$ {
try_files $uri = 404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
if (!-f $document_root$fastcgi_script_name) {
return 404;
}
include fastcgi_params;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SERVER_NAME $host;
fastcgi_pass unix:/var/run/php5-fpm-pool1.sock;
}
每个站点将请求传递给与其自己的池相关联的套接字。PHP 脚本作为池中指定的用户运行,并被锁定在我们为他们设置的文件权限中。
Note
在我们的 Nginx 配置中,我们加入了try_files $uri = 404;来防止 Nginx 手册中提到的攻击。3
通过将每个池锁定在自己的chroot监狱中,可以获得额外的安全层。请记住,您需要确保 PHP 需要访问的文件(比如日志目录或 ImageMagick 之类的二进制文件)在 jail 中是可用的。
会话安全性
您需要注意的两个重点领域是“会话劫持”和“会话固定”。除了本章之外,你应该学习 PHP 手册中关于会话安全的内容。
会话劫持
HTTP 是一种无状态协议,web 服务器可以同时为多个不同的访问者提供服务。
服务器需要能够区分客户机,并通过为每个客户机分配一个会话标识符来做到这一点。可以通过调用session_id()来检索会话标识符。它是在调用session_start()函数后创建的。
当客户端向服务器发出后续请求时,它们提供会话标识符,这允许服务器将请求与会话相关联。
客户端可以使用 cookies 或 URL 参数来提供会话。饼干是首选,但并不总是可用。如果 PHP 不能使用 cookie,它将自动透明地使用 URL,除非您在php.ini文件中设置了session.use_only_cookies设置。
很明显,如果您能够向服务器提供其他人的会话标识符,您就可以伪装成那个用户。
图 6-1 显示了恶意用户 Bob 能够拦截 Alice 发送给服务器的消息的场景。Bad Bob 读取请求并提取会话标识符(包含在 HTTP 请求的 cookie 标题中)。然后,他能够将该会话标识符呈现给服务器,服务器现在无法将他与 Alice 区分开。
图 6-1。
Bob steals Alice’s session identifier and masquerades as her
获得另一个用户的会话标识符可以通过几种方式实现。
- 如果会话标识符遵循一个可预测的模式,那么攻击者可以尝试确定它对用户来说是什么。PHP 使用一种非常随机的方式来生成会话标识符,所以您不需要担心这一点。
- 通过检查客户端和服务器之间的网络流量,攻击者可以读取会话标识符。您可以设置
session.cookie_secure=On使会话 cookies 仅在 HTTPS 可用,以减轻这种情况。HTTPS 还将加密被请求的 URL,因此如果会话标识符作为参数在请求中传递,它将被加密。 - 针对客户端的攻击,如 XSS 攻击或在其计算机上运行的特洛伊木马程序,也可能泄露会话标识符。这可以通过设置
session.cookie_httponly指令来部分缓解。
强制 PHP 只使用 cookies 不会减轻对这种攻击的利用。对手可以轻松设置 cookie 值。
会话固定
会话固定利用了 web 应用中的一个弱点。一些应用在对用户进行身份验证时不会为用户生成新的会话 ID。相反,它们允许使用现有的会话 ID。
当对手在 web 服务器上创建会话时,攻击就会发生。他们知道该会话的会话 ID。然后他们欺骗用户使用这个会话并验证自己。然后,攻击者能够使用已知的会话 ID,并拥有经过身份验证的用户的权限。
有几种方法可以设置会话 ID,实际使用的方法取决于应用如何接受标识符。
最简单的方法是在 URL 中传递会话标识符,就像这样的 http://example.org/index.php?PHPSESSID=1234 。
降低会话固定风险的最佳方法是在每次特权级别改变时调用函数session_regenerate_id(),例如在登录后。
您可以在配置文件中设置session.use_strict_mode=On。这个设置将强制 PHP 只使用它自己创建的会话标识符。它将拒绝用户提供的会话标识符。这将减少操纵 cookie 的企图。
设置session.use_cookies=On和session.use_only_cookies=On将阻止 PHP 接受来自 URL 的会话标识符。
提高会话安全性
不要依赖单一的策略来减少攻击,而是使用多层安全措施。除了我已经提到的缓解策略之外,您还应该执行以下操作:
- 检查 IP 地址在两次呼叫之间保持不变。对于在信号塔之间移动的移动电话来说,这并不总是可行的,因此在这样做之前,请检查您的用例。
- 使用短会话超时值来减少固定窗口。
- 为用户提供注销调用
session_destroy()的方法。
这些都不是特别有效,但每一个都有助于提高您的整体安全性。
跨站点脚本
跨站点脚本(XSS)攻击是将恶意代码注入良性站点的攻击。通常,像 JavaScript 这样的恶意浏览器端代码被放在网站上,供客户端下载和运行。
这种攻击是有效的,因为客户端认为代码来自它信任的网站。该代码可以访问会话标识符、cookies、HTML 存储数据和其他与站点相关的信息。
XSS 攻击有几种主要类型:存储攻击、反射攻击和 DOM 攻击。
在存储 XSS 攻击中,对手可以在服务器上的存储位置输入内容。例子可以是显示在网站上并存储在数据库中的用户评论。当网站向另一个访问者输出用户评论列表时,他们会收到恶意代码。
在反射 XSS 攻击中,对手可以让网站直接输出一些东西。这种攻击最常见的形式是表单填充错误,用以前提交的字段预先填充输入字段,或者输出错误的字段值。例如,通过将访问者发送到包含恶意代码的特制 URL 作为错误消息,攻击者可以欺骗客户端在受信任站点的上下文中执行该错误消息。
DOM 攻击完全发生在页面中。恶意代码是从页面中的元素读取的,对代码的调用是在页面本身中进行的。
此外,XSS 攻击可以分为服务器端或客户端攻击。服务器端攻击是指服务器发送恶意代码。当通过不安全的 JavaScript 调用使用不受信任的用户提供的数据更新 DOM 时,会发生客户端 XSS。
缓解 XSS 袭击
要遵循的最重要的规则是绝不允许将未转义的数据输出到客户端。在允许将数据发送到客户端之前,请始终过滤数据并去除有害标签。
Tip
记住这句口头禅“过滤输入,转义输出”。
三个有用的功能是htmlspecialchars()、htmlentities()和strip_tags()。有关如何使用这些函数来帮助减轻 XSS 的更多详细信息,请参阅本章后面的“转义输出”一节。
Tip
在显示输出之前对其进行转义的最安全的方法是使用filter_var($string, FILTER_SANITIZE_STRING)。
由于 URL 和 HTML 中可以使用各种各样的格式来输出数据,因此将代码列入黑名单是不安全的。您应该将您想要允许的特定标签列入白名单。看看 OWASP 过滤规避备忘单 4 看看有多少种方法可以规避黑名单。
您还需要在 HTML 页面的 JavaScript 中减少 XSS,但这超出了本手册的范围。
跨站点请求伪造
CSRF 攻击利用了网站对客户端的信任。在这些攻击中,对手诱骗客户端在信任该客户端的网站上执行命令。
最常见的表单是向表单输入发送一个POST请求。
假设 Alice 登录到她的银行网站,该网站有一个允许她向另一个账户转账的表单。Chuck 知道该表单的端点以及它有哪些输入字段。他设法骗过了 Alice 的网络浏览器,向该表单发送了一个POST请求,指示银行向他的账户转账。银行信任 Alice 的 web 浏览器,因为它有一个有效的会话并执行请求。
Chuck 欺骗 Alice 的 web 浏览器有很多方法,包括使用 iframes 和 JavaScript。
为了减少这些请求,您应该生成一个唯一的非常随机的令牌,并存储在 Alice 的会话中。当您输出表单时,您包括这个令牌,以便当 Alice 提交表单时,她也提交这个令牌。在处理表单之前,检查提交的令牌是否与存储在会话中的令牌相匹配。
Chuck 无法知道 Alice 会话中的令牌是什么,因此无法将其包含在他的POST中。您的代码将拒绝他诱骗 Alice 发出的请求,因为它没有有效的令牌。
实际的银行在执行敏感操作时通常会要求一个人重新进行身份认证,并且在此过程中通常会要求双重身份认证。
SQL 注入
SQL 注入是网络上最常见的攻击形式,也是最容易防御的攻击形式之一。当攻击者可以在 SQL 语句中插入恶意命令供数据库执行时,就会发生 SQL 注入。
许多数据库设置允许数据库将文件写入磁盘。该特性允许黑客通过使用数据库将 PHP 脚本写入 web 服务器将为其服务的目录来创建后门。
这意味着 SQL 注入的影响不仅限于损害您的数据库,还可能导致攻击者能够在您的数据库上执行任意代码。
SQL 注入的核心问题在于,一条 SQL 语句混合了数据和语法。通过允许用户提供的数据与函数语法结合,我们创造了恶意数据干扰语法的可能性。
准备好的陈述
开始减轻 PHP 语言中 SQL 注入的最有效的方法是专门使用准备好的语句与数据库进行交互。这将有助于排除大多数 SQL 注入攻击,但本身并不足以做到万无一失。
预准备语句非常重要,如果底层驱动程序不支持它们,PDO 驱动程序会模仿它们。
准备好的语句分三步工作:
- 设置带有数据占位符的语句。
- 将实际数据绑定到语句。
- 执行准备好的语句。
可以将新数据绑定到已经执行过的语句,然后用新语句再次运行它。数据库引擎不必再次解析 SQL,除了安全优势之外,这还提高了性能。
此代码给出了一个如何准备、绑定和执行语句的示例:
<?php
$stmt = $dbh->prepare("SELECT * FROM REGISTRY where name = ?");
$stmt->bindParam(':name', $_GET['name'], PDO::PARAM_STR, 12);
$stmt->execute();
Note
PDO::prepare()函数返回一个类型为PDOStatement的对象。
我们直接使用了GET变量,所以我们不需要对它进行转义,因为它被绑定为带有PDOStatement::bindParam()的变量,并且不能改变将要运行的 SQL 的语法。
PHP 中的其他数据库驱动程序也支持预处理语句。下面是 MySQL 5 手册中的一个例子:
/* Prepared statement, stage 1: prepare */
if (!($stmt = $mysqli->prepare("INSERT INTO test(id) VALUES (?)"))) {
echo "Prepare failed: (" . $mysqli->errno . ") " . $mysqli->error;
}
/* Prepared statement, stage 2: bind and execute */
$id = 1;
if (!$stmt->bind_param("i", $id)) {
echo "Binding parameters failed: (" . $stmt->errno . ") " . $stmt->error;
}
if (!$stmt->execute()) {
echo "Execute failed: (" . $stmt->errno . ") " . $stmt->error;
}
逃避
减轻 SQL 注入的一个不太有效的方法是在将特殊字符发送到数据库之前对其进行转义。这比使用预准备语句更容易出错。
如果你试图对特殊字符进行转义,你必须使用数据库特定的函数(如mysqli_real_escape_string()或PDO::quote(),而不是像addslashes()这样的通用函数。
总则
您还应该始终使用拥有应用运行所需的最少权限的用户连接到数据库。永远不要允许您的 web 应用以 root 用户的身份连接到数据库。
如果您在服务器上托管多个数据库,请为服务器上的每个数据库使用不同的用户,并确保他们的密码是唯一的。这将有助于防止一个站点上的 SQL 注入攻击影响其他站点的数据库。
确保您使用的是最新版本的 MySQL,并在客户端 DSN 中强制使用字符集。在某些易受攻击的编码方案中,有一种非常微妙的方法可以使用不匹配的字符集来部署 SQL 注入;参见本StackOverflow篇 6 的第二个答案(非公认答案)进行阐述。
远程代码注入
远程代码注入是一种攻击,对手可以让服务器包含并执行他们的代码。
将字符串作为代码计算的函数
像eval()、exec()和system()这样的函数容易受到远程代码注入攻击。如果您正在执行一个包含用户输入的变量,他们将能够使用转义字符插入命令。
您可以通过使用escapeshellargs()对传递给 shell 命令的参数进行转义来缓解这个问题。escapeshellcmd()函数将对 shell 命令本身进行转义。
Tip
如果您没有明确地使用这些函数,您应该在您的php.ini中禁用它们。这不是万无一失的,但它可以帮助。
assert()函数用于确保某个条件为真,如果不为真,则采取一些措施。它对于调试很有用,但是在生产中应该关闭它。您可以使用assert_options() 7 函数来配置 assert 的行为方式并将其关闭。
如果你给assert()传递一个字符串值,那么 PHP 将会像 PHP 代码一样计算这个字符串。这将允许攻击者在您的服务器上执行代码,如果他们能够控制您传递给assert()的参数的话。
<?php
function rce(string $a) {
assert($a);
}
rce('print("hello")'); // hello
在 PHP 7.2 中,向assert传递一个字符串是不赞成的,这段代码会生成一个警告,但是仍然会计算这个参数。
游戏包括并要求
如果 PHP 配置设置allow_url_include打开,include()和require()都允许包含 URL 指定的文件。
最常见的情况是,人们在 URL 中使用一个GET变量来确定要包含的一些动态内容。这是一个非常业余的错误。
例如,一个站点可以有一个像 http://example.com/index.php?sidebar=welcome 这样的 URL,然后动态地将welcome.php文件包含到侧边栏中。
对手可以提供一个 URL 而不是“welcome”字符串,并让他们自己的代码以与 web 服务器用户相同的权限级别在服务器上执行。
为了解决这类问题,您可以将allow_url_fopen转换为OFF,对您要包含的变量使用basename(),以便移除路径,并且只根据白名单进行包含。
<?php
$page = $_GET['page'];
$allowedPages = array('adverts','contacts','information');
if ( in_array($page, $allowedPages) ) {
include basename($page . '.html');
}
电子邮件注入
用户可以提供允许他们更改邮件正文或收件人列表的十六进制控制字符。
例如,如果您的表单允许此人输入其电子邮件地址作为电子邮件的“发件人”字段,则以下字符串将导致其他收件人被包括为邮件的抄送和密件抄送收件人:
sender@example.com%0ACc:target@email.com%0ABcc:anotherperson@emailexample.com,stranger@shouldhavefiltered.com
攻击者还可以提供自己的正文,甚至更改所发送消息的 MIME 类型。这意味着您的表单可能会被垃圾邮件发送者用来发送邮件。
您可以用几种方法来防止这种情况。
请确保您在发送邮件时正确过滤了输入内容。filter_var()函数提供了许多标志,您可以使用它们来确保您的输入数据符合期望的模式。
<?php
$from = $_POST["sender"];
$from = filter_var($from, FILTER_SANITIZE_EMAIL);
// send the email
您也可以安装并使用 Suhosin PHP 扩展。它提供了suhosin.mail.protect指令来防止这种情况。
你可以实现一个 tarpit 来减慢机器人的速度或者无限期地困住它们。看看 GitHub 8 上的msigley/PHP-HTTP-Tarpit作为一个 tarpit 的例子。
在设置邮件服务器时,您必须确保它没有被配置为允许 Internet 上的任何人使用它来发送邮件的开放中继。您还应该考虑关闭防火墙上的端口 25 (SMTP ),这样外部主机就无法到达您的服务器。
过滤器输入
在处理安全问题时,最好做好最坏情况的打算,并假设所有输入都被感染,并且所有用户行为都是恶意的。您应该只使用您已经手动确认安全的输入。
输入的格式可能会被过滤器忽略,然后被浏览器解析。我前面提到的 XSS 规避备忘单中有很多使用特殊字符来规避检测的例子。
输入可能使用非标准字符集,过滤功能可能无法正确理解该字符集。使用过滤 SQL 时,应该使用数据库本机过滤函数。
PHP 有一个非常强大的过滤功能,filter_var(),可以用来执行许多不同的过滤和消毒操作。你可以在 PHP 手册中找到过滤器的列表。
还有几个函数可以用来检查各种类型的字符串。它们有地区意识,因此会考虑语言字符。如果字符串只包含过滤器中的字符,函数将返回true,否则返回false。
| 功能 | 过滤 |
|---|---|
ctype_alnum() | 仅限字母数字字符 |
ctype_alpha() | 仅限字母字符 |
ctype_cntrl() | 字符串仅是控制字符 |
ctype_digit() | 字符串只能是数字 |
ctype_graph() | 仅可打印的字符和空格 |
ctype_lower() | 只有小写字母 |
ctype_print() | 可打印字符 |
ctype_punct() | 任何非空白或字母数字的可打印内容 |
ctype_space() | 检查空白字符 |
ctype_upper() | 只有大写字母 |
ctype_xdigit() | 十六进制数字 |
通常在客户端执行过滤,例如在浏览器中使用 JavaScript。这还不够,还必须在服务器端进行过滤和验证。
转义输出
编写安全 PHP 代码的基本规则之一是过滤输入并转义输出。
在发出数据之前,必须确保它对客户端是安全的。回想一下 XSS 攻击是如何工作的,作为一个例子来说明为什么您需要确保您发送给客户端的内容得到适当的处理。
如果您发送给客户端的数据包含执行代码的指令,那么它将盲目地执行代码。您必须确保只发送您打算让客户端执行的代码,而不是攻击者注入的代码。
与过滤输入一样,您不能依赖客户端来过滤发送给它的输出。并非所有客户端都启用了 JavaScript,黑客有可能绕过客户端过滤。
过滤输出最安全的方法是使用带有FILTER_SANITIZE_STRING标志的filter_var()。可能会有对您来说限制太多的用例,在这种情况下,您将需要查看像htmlspecialchars()、strip_tags()和htmlentities()这样的函数。
htmlspecialchars()和htmlentities()功能有相似的效果,你应该确保你理解不同之处。
不同之处在于,htmlentities()将编码任何具有 HTML 实体表示的东西,而htmlspecialchars()将只编码在 HTML 中有特殊意义的字符。
<?php
$string = '© 1982 Sinclair Research Ltd.';
echo htmlentities($string); // © 1982 Sinclair Research Ltd.
echo PHP_EOL;
echo htmlspecialchars($string); // © 1982 Sinclair Research Ltd.
此表显示了将由htmlspecialchars()转换的字符。
| 性格;角色;字母 | 成为 |
|---|---|
&(与号) | & |
"(双引号) | " |
'(单引号) | ' |
<(小于) | < |
>(大于) | > |
这两个函数都将一个标志作为第二个参数。您应该确保至少知道这三个标志,因为它们对于转义您输出的 JavaScript 非常重要:
| 旗 | 描述 |
|---|---|
ENT_COMPAT | 转换双引号,而不是单引号 |
ENT_QUOTES | 转换双引号和单引号 |
ENT_NOQUOTES | 不转换任何报价 |
当转义一个 JavaScript 字符串时,应该使用ENT_QUOTES标志。
字符串的编码可以在第三个参数中指定。在 PHP 7.1 中,这两个函数的默认编码都是 UTF-8。
避免原木中毒
如果您正在记录错误消息、信息消息等,您需要对您记录的内容采取一些预防措施。
显然,您绝不能记录用户密码或信用卡等敏感信息。如果你把它传递给一个日志记录函数,那么一定要混淆它。因此,信用卡号将是日志文件中的一系列星号,而不是实际的号码。
确保在记录之前过滤掉可执行代码和个人信息。
您还应该知道日志中毒攻击是如何工作的。该漏洞基于您的代码不正确地包含本地文件。如果您允许用户输入来确定包含哪个文件,那么攻击者可以操纵该输入来包含日志文件。如果日志文件包含恶意代码,那么它将被解释和运行。
攻击者需要做的就是将他们的代码放入您的日志文件,这非常容易做到。例如,他们可以通过创建一个请求,将包含他们想要运行的命令的字符串注入到日志中,来毒害您的 web 服务器日志。另一个例子是,攻击者可以 SSH 到您的服务器,并使用恶意代码作为他们的用户名来毒害您的身份验证日志文件。
为了帮助您理解影响,让我们来看一个漏洞利用的例子。假设您的代码运行在本地主机上,容易受到本地文件包含的攻击,并接受需要显示的图像的名称。
首先,我们使用命令nc localhost 80连接到 web 服务器。然后,我们向服务器发出以下请求:
GET /<?php passthru($_GET['cmd']); ?> HTTP/1.1
Host: localhost
Apache 将在日志文件中写一行,如下所示:
127.0.0.1 - - [08/Apr/2016:13:57:38 +0000]
"GET /<?php system($_GET['cmd']); ?>
HTTP/1.1" 400 226
"<?php passthru($_GET['cmd2']); ?>"
"<?php passthru($_GET['cmd']); ?>"
我把我的日志分成多行,但是很明显在你的日志文件中,它们都在同一行。
利用漏洞的下一步是向包含日志文件的站点发出请求(这要求您的站点中存在这样的漏洞)。
http://localhost/?file=/var/log/apache2/access.log&cmd=ls -la
很多事情都需要出错才能让你变得脆弱:
- web 服务器用户需要对目标日志文件的读取权限
- 您的代码必须允许攻击者包含目标文件
- 您不能在您的配置中禁用
exec、passthru和system
加密和哈希算法
加密和哈希是不同的概念,你应该确保你理解的区别。加密是双向操作;你可以加密和解密。哈希是一种单向操作,从设计上来说,获取哈希并将其转换为原始字符串是困难的或耗时的。
您应该将密码作为哈希存储在数据库中。这样,如果攻击者获得了您的数据库的副本,他们仍然无法获得用户密码,除非他们能够反转哈希。通常,反转散列会花费大量的时间,希望您有足够的时间来注意到安全漏洞,并提醒您的用户需要更改他们的密码。
计算一个散列值所需的时间将决定黑客通过暴力破解密码所需的时间。
PHP 中的加密
PHP 中的加密由mcrypt模块提供,需要单独安装和启用。mcrypt模块提供多种加密功能和常量。
可用的算法取决于安装 PHP 的操作系统。您不应该尝试编写自己的加密算法实现。
Zend 认证考试并不特别强调加密。
哈希函数
像 MD5 和 SHA1 这样的旧哈希算法计算速度非常快,所以你不能在任何涉及安全的地方使用它们。它们在编程的其他领域仍然非常有用,但不是在任何你依赖它们作为单向操作的地方。
PHP 5.5.0 引入了password_hash()函数,它提供了一种生成安全散列的便捷方式。
对于老版本的 PHP,应该使用crypt()函数。
默认情况下,password_hash()函数使用bcrypt算法来散列密码。bcrypt算法有一个参数,包括在返回散列结果之前应该对密码运行多少次。这被称为算法的“成本”。
通过增加算法必须运行的次数,可以增加计算哈希所需的时间。这意味着随着计算机变得更快,你可以增加你的bcrypt算法的迭代次数来保护你的密码免受暴力攻击。
您可以使用password_info()函数来检索关于如何计算散列的信息。这个函数会告诉你算法的名字,成本,和盐。
password_needs_rehash()函数将一个散列与您指定的选项进行比较,看它是否需要被重新散列。这将允许您更改用于散列密码的算法,例如随着时间的推移增加成本。
安全随机字符串和整数
PHP 有两个函数可以让你方便地生成加密安全的整数和字符串。这些函数可以在 PHP 运行的任何平台上运行。
| 功能 | 因素 | 返回 | 描述 |
|---|---|---|---|
random_bytes | Int $length | 字节串 | 生成一个长度为$length字节的随机字符串 |
random_int | Int $min,int $max | 随机整数 | 在$min和$max指定的范围内生成一个随机整数 |
下面是一个使用random_bytes的例子:
<?php
// get a string that contains 8 random bytes
$randomBytes = random_bytes(8);
$printableVersion = bin2hex($randomBytes);
echo $printableVersion; // d7e263202be1b99b
PHP 生成的字符串不一定是可打印的,所以我使用bin2hex()函数将其转换为十六进制字符串。十六进制需要两个字符来显示一个字节,所以我们最后输出的字符串是 16 个字符长(是我们生成的随机字节数的两倍)。
加盐密码
salt 字符串是添加到密码中的附加字符串。它应该为每个密码随机生成。它用于帮助字典攻击和预先计算的彩虹攻击变得更加困难。
您可以为password_hash()函数指定一个 salt,但是如果您忽略它,PHP 将为您创建一个。PHP 手册指出,预期的操作模式是让它为密码创建随机 salt。
crypt()函数接受一个 salt 字符串作为第二个参数,但是如果您不提供自己的 salt,它不会自动生成 salt。PHP 5.6.0+会发出通知,如果你没有提供一个盐。
检查密码
如果攻击者有可能精确地测量运行您的密码检查例程所需的时间,他们将能够收集到有助于他们破解密码的信息。这些攻击被称为定时攻击。
PHP 5.5.0 password_verify()函数是一个定时攻击9——比较password_hash()创建的哈希的安全方式。
如果您无法使用此功能,您需要计算用户提供的密码的哈希,然后将哈希与存储的哈希进行比较。比较哈希值容易受到计时攻击。
PHP 5.6.0 引入了hash_equals()函数,这是一种比较字符串的定时攻击安全的方法。在比较crypt()生成的散列时,应该使用这个函数。
错误消息的快速注释
你不应该向别人确认他们输入了不正确的用户名。您的错误消息应该是他们输入了不正确的用户名或密码。您向攻击者提供的信息越少,他们获得系统访问权限的时间就越长。
文件上传
文件上传是 web 应用的一个主要风险,需要通过多种方式来保护。
回想一下,$_FILES[]超级全局包含关于客户端上传的文件的信息。您应该将该数组中的所有内容都视为可疑,并确保手动确认每条信息。
PHP 处理文件上传的方式是将它们保存到一个临时目录中。您可以在那里对它们进行操作,然后将它们移动到您想要的位置。
您应该检查您正在处理的文件是一个有效的上传文件,并且客户端试图伪造它的文件名和在临时文件夹中的位置。
使用is_uploaded_file()功能来确保你所引用的文件确实被上传了。使用move_uploaded_file()而不是其他方法将它从临时目录移动到最终位置。
当引用一个文件时,使用basename()函数去掉路径,以防止有人盗用文件名。
不要信任用户指定的 MIME 类型。忽略用户提供的 MIME 类型,如果需要,使用finfo_file()来确定 MIME 类型。
如果你允许一个用户上传一张图片,你应该在上面使用一个 GD image 函数,比如getimagesize()来确认它是一张有效的图片。如果此功能失败,则该文件不是有效的图像。
生成自己的文件名来存储文件,不要使用用户提供的文件名。强烈建议对文件名使用随机散列,并通过检查 MIME 类型来手动设置扩展名。
确保存储文件的文件夹只允许 web 服务器用户访问。
如果您不需要提供上传的文件,那么将 uploads 文件夹放在文档根目录之外。
数据库存储
除了避免 SQL 注入,您还应该在与数据库交互时应用一些安全原则。
您应该为不同的代码环境分离数据库服务器。您的 QA、测试、开发和生产服务器应该都使用不同的数据库服务器,并且不应该能够访问彼此的数据库。
您必须阻止 Internet 访问您的数据库服务器。
这可以通过以下方法实现:使用防火墙关闭端口,禁止外部通信;使用没有路由到 Internet 的专用子网;或者将数据库服务器配置为仅侦听特定主机。
仅仅改变数据库监听的端口是不够的。我甚至认为这不值得麻烦,因为对攻击者来说它甚至不是减速带,只是让您的同事更难使用您的服务器环境。
如果在一台数据库服务器上运行多个应用,请确保每个应用在服务器上都有自己的用户名和密码。每个应用用户应该只拥有最少的权限,并且永远不能读取其他应用的数据库。
避免使用可预测的用户名,并确保使用安全的密码。例如,我通常使用随机生成的第 4 版 UUID 作为密码。
在将敏感数据放入数据库之前,使用mcrypt()和mhash()对其进行加密。
您应该不时地检查您的数据库日志。您将能够发现企图注入攻击和其他模式,这将让您识别漏洞或收紧代码区域。
避免在线发布您的密码
一个很好的建议是避免在网上发布你的数据库或 API 证书,这样人们就可以读到它们。好吧,我是在开玩笑,但说真的,你什么时候会把你所有的访问凭证公布给全世界和他的狗看?
您可以这样做的一种情况是提交一个 Git 存储库,并将其推送到 GitHub 或 Bitbucket 之类的服务。
确保任何配置文件都被您的版本控制系统忽略,并且永远不会被提交或推送到上游存储库。有一些从 GitHub 获取凭证的机器人会因为这些错误惩罚你。
与此链接相关的一个题外话是,您不应该将 Amazon 凭据硬编码到应用中。相反,设置一个 IAM 角色,允许访问您想要使用的服务,并将该角色应用到您的虚拟机。
Chapter 6 Quiz
Q1:display_error配置设置的推荐生产设置为On。
| 真实的 | | 错误的 |
Q2:使用 HTTPS 加密你的登录页面将有助于防止会话劫持和会话固定。
| 真实的 | | | 错误的 | * |
Q3:您可以通过使用 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 配置设置,强制将会话专门包含在 cookies 中。
| session.cookie_secure |
| session.use_cookies |
| session.use_trans_sid |
| 以上都不是 |
问题 4: CSRF 是指攻击者在用户不知情的情况下,诱骗用户的浏览器或设备发出请求。它利用了服务器对浏览器的信任。您可以通过在表单中包含一个 CSRF 令牌来避免这种情况,每当访问者加载页面时,该令牌就会增加 1。
| 真实的 | | 错误的 |
q5:crypt()和password_hash()函数都允许您指定 salt,但是如果您不指定,将会为您生成一个适当随机的 salt。
| 真实的 | | 错误的 |
Q6:浏览器通过操作系统调用来确定文件类型,并在请求中发送该信息。您可以相信这一点,以确定存储文件时使用的扩展名。
| 真实的 | | 错误的 |
Q7:因为 PHP 在结束运行时会删除临时文件,所以您应该首先确保使用copy()函数将临时文件放在一个永久的位置。
| 真实的 | | 错误的 |
Q8:默认情况下,PHP 被配置为能够包含存储在 URL 上的源代码。
| 真实的 | | 错误的 |
问题 9:防止 XSS 的一个充分的对策是在你的内容之前使用strip_tags()功能。
| 真实的 | | 错误的 |
Q10:除非 PHP 安全模式开启,否则open_basedir配置设置无效。它限制了 PHP 可以访问的目录。
| 真实的 | | 错误的 |
Footnotes 1
https://github.com/php/php-src/blob/master/php.ini-production
2
3
https://www.nginx.com/resources/wiki/start/topics/examples/phpfcgi/
4
https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
5
https://dev.mysql.com/doc/apis-php/en/apis-php-mysqli.quickstart.prepared-statements.html
6
https://stackoverflow.com/a/12202218/821275
7
https://secure.php.net/manual/en/function.assert-options.php
8
https://github.com/msigley/PHP-HTTP-Tarpit
9