Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ PHP NEWS
. Fixed bug GH-22681 (Reflection*::__toString() truncates on null bytes).
(DanielEScherzer)

- SOAP:
. Fixed header injection through the Content-Type context option, the
soapaction and the cookie names and values. (David Carlier)

- Sockets:
. Fixed socket_set_option() validation error messages for UDP_SEGMENT and
TCP_USER_TIMEOUT, and SO_LINGER options. (Weilin Du)
Expand Down
1 change: 1 addition & 0 deletions UPGRADING.INTERNALS
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ PHP 8.6 INTERNALS UPGRADE NOTES
php_stream_copy_to_stream_ex(). The mmap-based copy fallback was removed.
. Added zend_string_equals_cstr_ci().
. Added zend_string_ends_with() and related variants.
. Added trait support for internal classes.

========================
2. Build system changes
Expand Down
1 change: 1 addition & 0 deletions Zend/zend_API.h
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ ZEND_API zend_class_entry *zend_register_internal_class_ex(const zend_class_entr
ZEND_API zend_class_entry *zend_register_internal_class_with_flags(const zend_class_entry *class_entry, zend_class_entry *parent_ce, uint32_t flags);
ZEND_API zend_class_entry *zend_register_internal_interface(const zend_class_entry *orig_class_entry);
ZEND_API void zend_class_implements(zend_class_entry *class_entry, int num_interfaces, ...);
ZEND_API void zend_class_use_internal_traits(zend_class_entry *class_entry, int num_traits, ...);

ZEND_API zend_result zend_register_class_alias_ex(const char *name, size_t name_len, zend_class_entry *ce, bool persistent);

Expand Down
55 changes: 53 additions & 2 deletions Zend/zend_inheritance.c
Original file line number Diff line number Diff line change
Expand Up @@ -2403,7 +2403,11 @@ static void zend_add_trait_method(zend_class_entry *ce, zend_string *name, zend_
}
}

if (UNEXPECTED(fn->type == ZEND_INTERNAL_FUNCTION)) {
if (ce->type == ZEND_INTERNAL_CLASS) {
ZEND_ASSERT(fn->type == ZEND_INTERNAL_FUNCTION);
new_fn = (zend_function*)(uintptr_t)malloc(sizeof(zend_internal_function));
memcpy(new_fn, fn, sizeof(zend_internal_function));
} else if (UNEXPECTED(fn->type == ZEND_INTERNAL_FUNCTION)) {
new_fn = zend_arena_alloc(&CG(arena), sizeof(zend_internal_function));
memcpy(new_fn, fn, sizeof(zend_internal_function));
new_fn->common.fn_flags |= ZEND_ACC_ARENA_ALLOCATED;
Expand Down Expand Up @@ -2833,7 +2837,11 @@ static void zend_do_traits_constant_binding(zend_class_entry *ce, zend_class_ent
if (do_trait_constant_check(ce, constant, constant_name, traits, i)) {
zend_class_constant *ct = NULL;

ct = zend_arena_alloc(&CG(arena),sizeof(zend_class_constant));
if (ce->type == ZEND_INTERNAL_CLASS) {
ct = malloc(sizeof(zend_class_constant));
} else {
ct = zend_arena_alloc(&CG(arena),sizeof(zend_class_constant));
}
memcpy(ct, constant, sizeof(zend_class_constant));
constant = ct;

Expand Down Expand Up @@ -3012,6 +3020,49 @@ static void zend_do_traits_property_binding(zend_class_entry *ce, zend_class_ent
}
/* }}} */

ZEND_API void zend_class_use_internal_traits(zend_class_entry *class_entry, int num_traits, ...)
{
ZEND_ASSERT(class_entry->ce_flags & ZEND_ACC_LINKED);
ZEND_ASSERT(num_traits >= 0);

if (UNEXPECTED(num_traits == 0)) {
return;
}

zend_class_entry **traits = safe_pemalloc(num_traits, sizeof(zend_class_entry *), 0, /* persistent */ true);
class_entry->trait_names = safe_pemalloc(num_traits, sizeof(zend_class_name), 0, /* persistent */ true);
class_entry->num_traits = num_traits;

va_list trait_list;
va_start(trait_list, num_traits);
for (int i = 0; i < num_traits; i++) {
zend_class_entry *trait_entry = va_arg(trait_list, zend_class_entry *);
class_entry->trait_names[i].name = zend_string_copy(trait_entry->name);
class_entry->trait_names[i].lc_name = zend_string_tolower_ex(zend_string_copy(trait_entry->name), /* persistent */ true);

if (UNEXPECTED(!(trait_entry->ce_flags & ZEND_ACC_TRAIT))) {
free(traits);
zend_error_noreturn(E_COMPILE_ERROR, "Class %s cannot use %s - it is not a trait",
ZSTR_VAL(class_entry->name), ZSTR_VAL(trait_entry->name));
}
traits[i] = trait_entry;
}
va_end(trait_list);

bool contains_abstract_methods = false;
zend_do_traits_method_binding(class_entry, traits, NULL, NULL, false, &contains_abstract_methods);
zend_do_traits_constant_binding(class_entry, traits);
zend_do_traits_property_binding(class_entry, traits);

ZEND_HASH_MAP_FOREACH_PTR(&class_entry->function_table, zend_function *fn) {
zend_fixup_trait_method(fn, class_entry);
} ZEND_HASH_FOREACH_END();

free(traits);

/* TODO: Verify abstract trait method implementation requirements are enforced. */
}

#define MAX_ABSTRACT_INFO_CNT 3
#define MAX_ABSTRACT_INFO_FMT "%s%s%s%s"
#define DISPLAY_ABSTRACT_FN(idx) \
Expand Down
9 changes: 8 additions & 1 deletion Zend/zend_opcode.c
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ ZEND_API void destroy_zend_class(zval *zv)
zend_string_release_ex(ce->name, 1);

ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, fn) {
if (fn->common.scope == ce) {
if (fn->common.scope == ce && !(fn->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) {
zend_free_internal_arg_info(&fn->internal_function, true);

if (fn->common.attributes) {
Expand Down Expand Up @@ -535,6 +535,13 @@ ZEND_API void destroy_zend_class(zval *zv)
if (ce->attributes) {
zend_hash_release(ce->attributes);
}
if (ce->num_traits > 0) {
for (uint32_t i = 0; i < ce->num_traits; i++) {
zend_string_release(ce->trait_names[i].name);
zend_string_release(ce->trait_names[i].lc_name);
}
free(ce->trait_names);
}
free(ce);
break;
}
Expand Down
29 changes: 29 additions & 0 deletions build/gen_stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\Node\Stmt\TraitUse;
use PhpParser\PrettyPrinter\Standard;
use PhpParser\PrettyPrinterAbstract;

Expand Down Expand Up @@ -3383,6 +3384,7 @@ class ClassInfo {
* @param AttributeInfo[] $attributes
* @param Name[] $extends
* @param Name[] $implements
* @param Name[] $uses
* @param ConstInfo[] $constInfos
* @param PropertyInfo[] $propertyInfos
* @param FuncInfo[] $funcInfos
Expand All @@ -3401,6 +3403,7 @@ public function __construct(
private bool $isNotSerializable,
private readonly array $extends,
private readonly array $implements,
private readonly array $uses,
public /* readonly */ array $constInfos,
private /* readonly */ array $propertyInfos,
public array $funcInfos,
Expand All @@ -3421,6 +3424,9 @@ public function getRegistration(array $allConstInfos): string
foreach ($this->implements as $implements) {
$params[] = "zend_class_entry *class_entry_" . implode("_", $implements->getParts());
}
foreach ($this->uses as $use) {
$params[] = "zend_class_entry *class_entry_" . implode("_", $use->getParts());
}

$escapedName = implode("_", $this->name->getParts());

Expand Down Expand Up @@ -3518,6 +3524,17 @@ function (Name $item) {
$code .= "\tzend_class_implements(class_entry, " . count($implements) . ", " . implode(", ", $implements) . ");\n";
}

$traits = array_map(
function (Name $item) {
return "class_entry_" . implode("_", $item->getParts());
},
$this->uses
);

if (!empty($traits)) {
$code .= "\tzend_class_use_internal_traits(class_entry, " . count($traits) . ", " . implode(", ", $traits) . ");\n";
}

if ($this->alias) {
$code .= "\tzend_register_class_alias(\"" . str_replace("\\", "\\\\", $this->alias) . "\", class_entry);\n";
}
Expand Down Expand Up @@ -4408,6 +4425,7 @@ private function handleStatements(array $stmts, PrettyPrinterAbstract $prettyPri
$propertyInfos = [];
$methodInfos = [];
$enumCaseInfos = [];
$traitUses = [];
foreach ($stmt->stmts as $classStmt) {
$cond = self::handlePreprocessorConditions($conds, $classStmt);
if ($classStmt instanceof Stmt\Nop) {
Expand Down Expand Up @@ -4469,6 +4487,13 @@ private function handleStatements(array $stmts, PrettyPrinterAbstract $prettyPri
$classStmt->expr,
$classStmt->expr ? $prettyPrinter->prettyPrintExpr($classStmt->expr) : null,
);
} else if ($classStmt instanceof TraitUse) {
if ($classStmt->adaptations) {
throw new Exception("Trait adaptations are not supported");
}
foreach ($classStmt->traits as $trait) {
$traitUses[] = $trait;
}
} else {
throw new Exception("Not implemented {$classStmt->getType()}");
}
Expand All @@ -4481,6 +4506,7 @@ private function handleStatements(array $stmts, PrettyPrinterAbstract $prettyPri
$propertyInfos,
$methodInfos,
$enumCaseInfos,
$traitUses,
$cond,
$this->getMinimumPhpVersionIdCompatibility(),
$this->isUndocumentable
Expand Down Expand Up @@ -5167,6 +5193,7 @@ function parseProperty(
* @param PropertyInfo[] $properties
* @param FuncInfo[] $methods
* @param EnumCaseInfo[] $enumCases
* @param Name[] $traitUses
*/
function parseClass(
Name $name,
Expand All @@ -5175,6 +5202,7 @@ function parseClass(
array $properties,
array $methods,
array $enumCases,
array $traitUses,
?string $cond,
?int $minimumPhpVersionIdCompatibility,
bool $isUndocumentable
Expand Down Expand Up @@ -5247,6 +5275,7 @@ function parseClass(
$isNotSerializable,
$extends,
$implements,
$traitUses,
$consts,
$properties,
$methods,
Expand Down
2 changes: 1 addition & 1 deletion ext/filter/sanitizing_filters.c
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ static void php_filter_encode_url(zval *value, const unsigned char* chars, const
const unsigned char *e = s + char_len;
zend_string *str;

memset(tmp, 1, sizeof(tmp)-1);
memset(tmp, 1, sizeof(tmp));

while (s < e) {
tmp[*s++] = '\0';
Expand Down
12 changes: 12 additions & 0 deletions ext/filter/tests/filter_sanitize_encoded_0xff.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
FILTER_SANITIZE_ENCODED percent-encodes 0xFF
--EXTENSIONS--
filter
--FILE--
<?php
var_dump(filter_var("\xFF", FILTER_SANITIZE_ENCODED));
var_dump(filter_var("\xFE\xFF\x00A", FILTER_SANITIZE_ENCODED));
?>
--EXPECT--
string(3) "%FF"
string(10) "%FE%FF%00A"
25 changes: 13 additions & 12 deletions ext/soap/php_http.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,24 @@ static zend_string *get_http_headers(php_stream *socketd);
#define smart_str_append_const(str, const) \
smart_str_appendl(str,const,sizeof(const)-1)

static void soap_smart_str_append_header_value(smart_str *dest, const zend_string *value, const char *header_name)
static void soap_smart_str_append_header_value_ex(smart_str *dest, const char *src, size_t len, const char *header_name)
{
const char *src = ZSTR_VAL(value);
size_t len = ZSTR_LEN(value);
size_t i = 0;
while (i < len && src[i] != '\r' && src[i] != '\n') {
i++;
}
smart_str_appendl(dest, src, i);
if (i < len) {
smart_str_appendl(dest, src, i);
php_error_docref(NULL, E_WARNING,
"Header %s value contains newline characters and has been truncated", header_name);
} else {
smart_str_append(dest, value);
}
}

static void soap_smart_str_append_header_value(smart_str *dest, const zend_string *value, const char *header_name)
{
soap_smart_str_append_header_value_ex(dest, ZSTR_VAL(value), ZSTR_LEN(value), header_name);
}

/* Proxy HTTP Authentication */
bool proxy_authentication(zval* this_ptr, smart_str* soap_headers)
{
Expand Down Expand Up @@ -656,13 +657,13 @@ bool make_http_soap_request(
Z_STRLEN_P(tmp) > 0
) {
smart_str_append_const(&soap_headers, "Content-Type: ");
smart_str_append(&soap_headers, Z_STR_P(tmp));
soap_smart_str_append_header_value(&soap_headers, Z_STR_P(tmp), "Content-Type");
} else {
smart_str_append_const(&soap_headers, "Content-Type: application/soap+xml; charset=utf-8");
}
if (soapaction) {
smart_str_append_const(&soap_headers,"; action=\"");
smart_str_appends(&soap_headers, soapaction);
soap_smart_str_append_header_value_ex(&soap_headers, soapaction, strlen(soapaction), "SOAPAction");
smart_str_append_const(&soap_headers,"\"");
}
smart_str_append_const(&soap_headers,"\r\n");
Expand All @@ -673,14 +674,14 @@ bool make_http_soap_request(
Z_STRLEN_P(tmp) > 0
) {
smart_str_append_const(&soap_headers, "Content-Type: ");
smart_str_append(&soap_headers, Z_STR_P(tmp));
soap_smart_str_append_header_value(&soap_headers, Z_STR_P(tmp), "Content-Type");
smart_str_append_const(&soap_headers, "\r\n");
} else {
smart_str_append_const(&soap_headers, "Content-Type: text/xml; charset=utf-8\r\n");
}
if (soapaction) {
smart_str_append_const(&soap_headers, "SOAPAction: \"");
smart_str_appends(&soap_headers, soapaction);
soap_smart_str_append_header_value_ex(&soap_headers, soapaction, strlen(soapaction), "SOAPAction");
smart_str_append_const(&soap_headers, "\"\r\n");
}
}
Expand Down Expand Up @@ -892,9 +893,9 @@ bool make_http_soap_request(
smart_str_appends(&soap_headers, "; ");
}
first_cookie = false;
smart_str_append(&soap_headers, key);
soap_smart_str_append_header_value(&soap_headers, key, "Cookie");
smart_str_appendc(&soap_headers, '=');
smart_str_append(&soap_headers, Z_STR_P(value));
soap_smart_str_append_header_value(&soap_headers, Z_STR_P(value), "Cookie");
}
}
}
Expand Down
62 changes: 62 additions & 0 deletions ext/soap/tests/content_type_header_injection.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
--TEST--
SoapClient must truncate a content_type context option that contains newline characters
--EXTENSIONS--
soap
--SKIPIF--
<?php
if (!file_exists(__DIR__ . "/../../../sapi/cli/tests/php_cli_server.inc")) {
echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
}
?>
--FILE--
<?php

include __DIR__ . "/../../../sapi/cli/tests/php_cli_server.inc";

$args = ["-d", "extension_dir=" . ini_get("extension_dir"), "-d", "extension=" . (substr(PHP_OS, 0, 3) == "WIN" ? "php_" : "") . "soap." . PHP_SHLIB_SUFFIX];
if (php_ini_loaded_file()) {
$args[] = "-c";
$args[] = php_ini_loaded_file();
}
$code = <<<'PHP'
$content = trim(file_get_contents("php://input")) . PHP_EOL;
PHP;

php_cli_server_start($code, null, $args);

$context = stream_context_create([
'http' => ['content_type' => "text/xml\r\nX-Injected: yes"],
]);

foreach ([SOAP_1_1, SOAP_1_2] as $version) {
$client = new SoapClient(NULL, [
'location' => 'http://' . PHP_CLI_SERVER_ADDRESS,
'uri' => 'misc-uri',
'trace' => true,
'soap_version' => $version,
'stream_context' => $context,
]);

$client->__soapCall("foo", []);
echo $client->__getLastRequestHeaders();
}

?>
--EXPECTF--
Warning: SoapClient::__doRequest(): Header Content-Type value contains newline characters and has been truncated in %s on line %d
POST / HTTP/1.1
Host: localhost:%d
Connection: Keep-Alive
User-Agent: PHP-SOAP/%s
Content-Type: text/xml
SOAPAction: "misc-uri#foo"
Content-Length: %d


Warning: SoapClient::__doRequest(): Header Content-Type value contains newline characters and has been truncated in %s on line %d
POST / HTTP/1.1
Host: localhost:%d
Connection: Keep-Alive
User-Agent: PHP-SOAP/%s
Content-Type: text/xml; action="misc-uri#foo"
Content-Length: %d
Loading