Skip to content

Tests: cover the library and fix the bugs found - #158

Merged
f3l1x merged 71 commits into
masterfrom
fix/bugs-found-by-tests
Oct 4, 2026
Merged

f3l1x merged 71 commits into
masterfrom
fix/bugs-found-by-tests

Conversation

@f3l1x

@f3l1x f3l1x commented Oct 4, 2026

Copy link
Copy Markdown
Member

Adds tests for most of the library and fixes the bugs they found. Each fix is a separate commit with a regression test that fails without the fix. The full suite passes at every commit.

Line coverage goes from 52% to 81% for nusoap.php and from 37% to 93% for nusoapmime.php. PHPStan errors drop from 706 to 692.

Crashes on PHP 8

  • MIME server: every response sent without attachments failed with count(null).
  • Missing classes: a method name like a..b..c naming a missing namespaced class threw a TypeError, so any client could crash the server.
  • WSDL without operations: a request to a server whose WSDL had no operations registered threw a TypeError.
  • soapval with attributes: a scalar soapval with attributes and an XSD type threw a TypeError.
  • WSDL cache: a missing or unwritable cache directory made flock(false) throw a TypeError.
  • Missing schema file: threw a TypeError instead of reporting the error.
  • serializeTypeDef(): threw a TypeError on every call.
  • Truncated chunked body: decodeChunked() threw a ValueError.

Security

  • Proxy code injection: getProxy() evaluated code built from unescaped WSDL values, so a WSDL could run arbitrary PHP.
  • Faults overwriting client state: every child element of a received fault was copied to the client object, so a server could overwrite e.g. the endpoint of the next call.
  • Cookie scope: cookies were sent to other domains (example.com cookies to notexample.com) and to other paths (/app cookies to /application).

Wrong results

  • UTC timestamps: timestamp_to_iso8601() in UTC mode was off by the offset of the default timezone.
  • Charset parsing: the charset was taken as everything after the first = of the Content-Type. Valid UTF-8 messages with further parameters were rejected, on both the client and the server.
  • Truncated XML was taken as successfully parsed.
  • Bogus attachments: XML responses with lines starting with -- or containing Content-Type: got bogus attachments.
  • Digest authentication:
    • opaque was placed inside uri.
    • nc never increased.
    • nc was hashed as decimal but sent as hex.
    • A qop list from the server was echoed back verbatim.
    • The challenge was split on commas inside quoted values.
  • Malformed generated XML:
    • Values were left unescaped for non-string XSD types, simple types and simple content.
    • Values were left unescaped in the header and document XML the parser rebuilds.
    • Referenced attributes were written as urn:x:lang="...".
    • Arrays were written as the text Array for scalar types.
  • Schema serialization: extensions were dropped and local attributes leaked into the global ones.
  • WSDL parsing: a bogus binding entry was stored on the last portType operation.
  • Array types: arrays of objects, nulls and floats declared invalid or mismatched array types.
  • Custom scalar types: a scalar with a custom type in another namespace was serialized as xsd:string.

Interoperability

  • Compression:
    • zlib wrapped deflate, raw deflate and gzip with optional header fields are now decoded.
    • Content-Encoding is read case-insensitively.
    • Chunk extensions are accepted.
  • Redirects:
    • Relative redirects and 308 are followed.
    • A persistent connection is not reused after a redirect to another server.
    • An external HTTPS WSDL is redirected instead of fetched.
  • URLs:
    • Uppercase schemes (HTTPS://) are accepted.
    • Set-Cookie attributes are read case-insensitively.
  • WSDL imports are resolved relative to a local WSDL file, for absolute paths, and keep their query (?xsd=xsd0).
  • Messages: an empty SOAP Body, undeclared namespace prefixes, dateTime without timezone, faults without faultstring and invalid status lines are now handled.

Warnings and deprecations removed

  • Dynamic properties for URLs with credentials or a fragment.
  • SERVER_PORT missing in configureWSDL().
  • WSDL serialization and lookups without operations or with unknown bindings.
  • Missing content type in the MIME client and server.
  • "Array to string conversion" in debug messages.

Other fixes

  • A request that is not text/xml now gets a proper client fault.
  • The web description page escapes its values.
  • remove() in the WSDL cache releases only the lock it obtained.

Tests

  • New test files for:
    • Transport: request building, cookies, digest auth, chunked bodies.
    • Client: responses, settings, calls, proxy generation.
    • Parser.
    • XML Schema.
    • WSDL type serialization and imports.
    • soapval, the WSDL cache and MIME attachments.
    • Server request handling.
    • Date helpers.
  • FakeHttpStream now keeps connections with a known response length alive, so retried and redirected requests can be tested.

… MIME

Raises line coverage of nusoap.php from 52% to 73% and of nusoapmime.php
from 37% to 89%, focusing on logic without network access.
nusoap_server_mime::$responseAttachments had no default, so sending a
response without attachments called count(null), a TypeError on PHP 8.
- opaque was written inside the quoted uri value
- the nonce count never increased, as it was post-incremented
- the nonce count was hashed as decimal but sent as hexadecimal
- a qop list offered by the server (auth,auth-int) was echoed back,
  auth is now picked, and qop is sent as a token (RFC 2617)
- without qop, empty qop, nc and cnonce were sent and a missing qop
  key raised a warning (RFC 2069)
- the WWW-Authenticate challenge was split on every comma, breaking
  quoted values containing commas

FakeHttpStream now keeps a connection with a known response length
alive, so the challenge and the retried request can be tested.
setURL() copies every parse_url() part to a property, so a URL with
credentials or a fragment created dynamic properties, deprecated since
PHP 8.2.
parseCookie() searched for lowercase "domain=", "path=", "expires=" and
"secure" anywhere in the header. Capitalized attributes (Path, Domain,
Secure), common in server responses, were ignored, and a cookie value
containing "path=" was taken as the path. Attributes are now parsed one
by one, with case-insensitive names as required by RFC 6265.
parseFile() passed the result of file() to join(), which throws a
TypeError on PHP 8 when the file cannot be read, so the "Error reading
XML" error was never set.
- complexType extensions were serialized without their complexContent
  or simpleContent extension, dropping the base type
- attributes declared or referenced inside a complexType were also
  registered as global attributes, so serializeSchema() declared them
  again at the top level and same-named attributes of different types
  overwrote each other
A scalar with a type from another namespace, e.g. new soapval('code',
'Currency', 'EUR', false, 'urn:types'), was serialized as xsd:string
with an unused namespace declaration. Only arrays and objects used the
given type.
- chunk-size lines with an extension (RFC 7230), e.g. "5;name=value",
  raised a deprecation from hexdec() in both getResponse() and
  decodeChunked()
- decodeChunked() threw a ValueError on a truncated body instead of
  returning the data received so far
configureWSDL() read SERVER_PORT and SCRIPT_NAME whenever SERVER_NAME
was set. Without them, an undefined key warning was raised and the
empty port produced a stray colon in the endpoint URL.
getProxy() evaluates PHP code generated from the WSDL. Operation names,
part names, types, the namespace and the SOAPAction were concatenated
into it unescaped, so a WSDL could run arbitrary code, e.g. with a
quote in a soapAction. Values are now exported as PHP literals, names
must be PHP identifiers and comments cannot be terminated early.
Covers calling functions and class methods, faults, header and charset
parsing, compressed requests and responses, the debug log and serving
the WSDL and web description.
parseRequest() only set an error for a wrong or missing content type,
so the request was processed with an empty method name and the client
got a misleading "method '' not defined" fault.
A method name with nested delimiters, e.g. Ns..Class..method, is called
on a namespaced class. When the class did not exist, get_class_methods()
threw a TypeError on PHP 8, so any client could crash the server.
The gzip header was skipped as a fixed 10 bytes, so a request with an
original file name or other optional header fields could not be
decoded, and invalid data raised a warning from gzinflate().
HTTP deflate is zlib wrapped data, but many implementations send raw
deflate data, as nusoap_server itself does for responses. Such requests
were rejected.
A request with "Content-Encoding: GZIP" was not decoded and failed to
parse, although content codings are case-insensitive (RFC 7231).
"Content-Encoding: GZIP" passed the case-insensitive check, but the
payload was only decoded for the lowercase values, so the compressed
data was passed to the XML parser.
HTTP deflate is zlib wrapped data (RFC 7230), but only raw deflate
data was accepted, so responses of standard compliant servers failed
with "Error using gzinflate to inflate the payload".
The gzip header was skipped as a fixed 10 bytes, so responses with an
original file name or other optional header fields could not be
decoded.
The result of the final xml_parse() call was ignored, so an incomplete
document, e.g. a truncated response, was taken as successfully parsed,
returned null and raised undefined key warnings.
An envelope without an element in the Body, e.g. the response of a
one-way operation, raised undefined key warnings.
An element with a prefix without a namespace declaration raised
undefined key warnings. Its namespace is now empty.
Serializing the WSDL before any operation was registered raised
undefined key and invalid foreach warnings.
A top-level element such as <binding> was first handled by the section
before it. It was stored as a bogus "binding" entry of the last portType
operation, and after an empty portType it used null as an array offset,
deprecated since PHP 8.5.
nusoap_server_mime::parseRequest() read the content-type header without
checking it, raising undefined key warnings before the missing content
type was reported.
nusoap_client_mime::parseResponse() read the content-type header
without checking it, raising undefined key warnings before the missing
content type was reported.
Only URLs containing "http://" were redirected, so an https:// WSDL
URL was fetched and passed through instead, which fails without
allow_url_fopen.
The service name, operation names, parts and documentation were written
to the HTML page unescaped, so e.g. documentation mentioning <element>
was rendered as a tag and & made the page invalid.
getOperationData() passed the missing operations of a binding to
array_keys(), a TypeError on PHP 8, e.g. when a request reached a
server whose WSDL had no operations registered.
Only xsd:string values were escaped, so e.g. an xsd:anyURI with a query
containing "&" produced malformed XML.
The "!" value of a complexType with simpleContent was written
unescaped, so special characters produced malformed XML.
Values of a simpleType defined in the WSDL, e.g. an enumeration, were
written unescaped, so special characters produced malformed XML.
The debug message for an element without type interpolated its value,
raising an "Array to string conversion" warning for an array value.
The debug message for each map entry interpolated its value, raising an
"Array to string conversion" warning for an array value.
The debug message for each repeated element without type interpolated
its value, raising an "Array to string conversion" warning for an array
value.
An attribute referenced by ref="tns:lang" was written with its namespace
qualified name, e.g. urn:svc:lang="...", which is malformed XML. It is now
written as tns:lang and its value is also taken from "!tns:lang".
The SOAP header and document XML is rebuilt from the parsed elements,
but the unescaped character data was appended, so e.g. a header value
"x&amp;y" was returned by getHeaders() as "x&y", which is malformed XML.
Attribute values were written unescaped to the rebuilt header and
document XML, so e.g. a value containing a quote produced malformed XML.
The cookie domain was matched as a suffix of the host, so a cookie for
example.com was also sent to notexample.com. The host now has to be the
domain or one of its subdomains (RFC 6265).
The cookie path was matched as a case-insensitive prefix of the request
path including its query, so a cookie for /app was also sent to
/application. The request path now has to be the cookie path or below
it (RFC 6265).
When the cache directory did not exist or was not writable, fopen() of
the lock file failed with a warning and flock(false) threw a TypeError on
PHP 8, so a misconfigured cache broke every client.
remove() ignores failures to obtain the lock, but released it anyway. A
lock held by the caller was released early, and without any lock the
unset handle was passed to flock().
A relative Location header (RFC 7231) was passed to setURL() as is, so
e.g. "/other" raised undefined key warnings and "next.php" was sent as
the request target. The location is now resolved against the current
URL.
Only 301, 302 and 307 redirects were followed. A 308 response (RFC 7538)
failed with "no data present after HTTP headers".
The debug message of send() read the content-type header without
checking it, raising an undefined key warning before the missing content
type was reported.
A response whose first line was not an HTTP status line, e.g. from a
server not speaking HTTP, raised an undefined key warning and was
processed as status 0.
With a persistent connection, a redirect to another host, port or
scheme sent the request again over the connection to the previous
server.
Covers rpc/encoded, rpc/literal, document/literal and literal wrapped
calls, string parameters, SOAP headers and the SOAPAction header.
serializeTypeDef() read the target namespace from the schema file name,
a TypeError on PHP 8, and attributes and elements without checking them.
Local elements, which are no types of their own, are now rendered as
empty elements instead of being dropped, and the "^" marking an element
is not part of the element name.
The array type of an array of objects was "object", which is not a
type. Objects are serialized as structs, so they are declared like an
array of associative arrays.
The array type of an array of null values was "NULL", which is not a
type.
An array of floats was declared as xsd:double[n], while its items were
serialized as xsd:float.
Every parsed message was scanned for MIME boundaries and headers, so
an XML response with a text line starting with "--" or containing
"Content-Type:" got bogus attachments. Only a payload that fails to parse
as XML can be a multipart message.
A MIME header line before the first boundary of a payload that is not
XML was stored as an attachment with the key -1.
An array value for e.g. xsd:int was serialized as the text "Array" with
an "Array to string conversion" warning. It is now an error, which the
server reports as "unable to serialize result".
@codecov

codecov Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.35484% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.56%. Comparing base (4f31fa0) to head (2569248).

Files with missing lines Patch % Lines
src/nusoap.php 94.21% 14 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           master     #158       +/-   ##
===========================================
+ Coverage   50.95%   81.56%   +30.61%     
===========================================
  Files           2        2               
  Lines        3839     3921       +82     
===========================================
+ Hits         1956     3198     +1242     
+ Misses       1883      723     -1160     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Mail_mimePart::encode() of pear/mail_mime 1.10.0 and 1.10.1 counts the
uninitialized subparts of a part without subparts, a TypeError on PHP 8,
which failed the MIME attachment tests with --prefer-lowest.
@f3l1x
f3l1x merged commit d38d5db into master Oct 4, 2026
10 checks passed
@f3l1x
f3l1x deleted the fix/bugs-found-by-tests branch October 4, 2026 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant