Xslt: xml_external_entities directive

Loading of external entities defined in the internal DTD subset,
that is, in the XML document itself, is now disabled by default, and
can be re-enabled with "xml_external_entities on;".  This makes
processing of untrusted XML responses with the xslt module slightly
safer (though still not recommended unless you thoughtfully considered
risks).

To prevent loading we intercept entities defined in the internal
subset via the entityDecl callback, and remove system identifiers from
entities.  This ensures that entities cannot be loaded directly, but
still allows using of public entities with appropriate system XML
catalog.

Additionally, since libxml2 before 2.14.0 (Mar 27 2025) accepts
in-document catalogs by default, these are explicitly disabled.

Signed-off-by: Vadim Zhestikov <v.zhestikov@f5.com>
Origin: https://freenginx.org/hg/nginx/rev/94dae9ab1018
This commit is contained in:
Maxim Dounin 2025-11-09 12:01:38 +03:00 • committed by VadimZhestikov
parent 017dbad856
commit 4d0e620f9a

View file

@ -11,6 +11,7 @@
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <libxml/xmlversion.h>
#include <libxslt/xslt.h>
#include <libxslt/xsltInternals.h>
#include <libxslt/transform.h>
@ -21,6 +22,10 @@
#include <libexslt/exslt.h>
#endif
#if (defined LIBXML_CATALOG_ENABLED && LIBXML_VERSION < 21400)
#include <libxml/catalog.h>
#endif
#ifndef NGX_HTTP_XSLT_REUSE_DTD
#define NGX_HTTP_XSLT_REUSE_DTD 1
@ -59,6 +64,7 @@ typedef struct {
ngx_array_t *types_keys;
ngx_array_t *params; /* ngx_http_xslt_param_t */
ngx_flag_t last_modified;
ngx_flag_t external_entities;
} ngx_http_xslt_filter_loc_conf_t;
@ -81,6 +87,9 @@ static ngx_int_t ngx_http_xslt_add_chunk(ngx_http_request_t *r,
static void ngx_http_xslt_sax_external_subset(void *data, const xmlChar *name,
const xmlChar *externalId, const xmlChar *systemId);
static void ngx_http_xslt_sax_entity_decl(void *data, const xmlChar *name,
int type, const xmlChar *publicId, const xmlChar *systemId,
xmlChar *content);
static void ngx_cdecl ngx_http_xslt_sax_error(void *data, const char *msg, ...);
@ -124,6 +133,13 @@ static ngx_command_t ngx_http_xslt_filter_commands[] = {
0,
NULL },
{ ngx_string("xml_external_entities"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_xslt_filter_loc_conf_t, external_entities),
NULL },
{ ngx_string("xslt_stylesheet"),
NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
ngx_http_xslt_stylesheet,
@ -385,6 +401,7 @@ ngx_http_xslt_add_chunk(ngx_http_request_t *r, ngx_http_xslt_filter_ctx_t *ctx,
|XML_PARSE_NONET|XML_PARSE_NOWARNING);
ctxt->sax->externalSubset = ngx_http_xslt_sax_external_subset;
ctxt->sax->entityDecl = ngx_http_xslt_sax_entity_decl;
ctxt->sax->setDocumentLocator = NULL;
ctxt->sax->error = ngx_http_xslt_sax_error;
ctxt->sax->fatalError = ngx_http_xslt_sax_error;
@ -460,6 +477,64 @@ ngx_http_xslt_sax_external_subset(void *data, const xmlChar *name,
}
static void
ngx_http_xslt_sax_entity_decl(void *data, const xmlChar *name, int type,
const xmlChar *publicId, const xmlChar *systemId, xmlChar *content)
{
xmlParserCtxtPtr ctxt = data;
ngx_http_request_t *r;
ngx_http_xslt_filter_ctx_t *ctx;
ngx_http_xslt_filter_loc_conf_t *conf;
ctx = ctxt->sax->_private;
r = ctx->request;
ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"xslt filter entityDecl: \"%s\" \"%s\" \"%s\"",
name ? name : (xmlChar *) "",
publicId ? publicId : (xmlChar *) "",
systemId ? systemId : (xmlChar *) "");
conf = ngx_http_get_module_loc_conf(r, ngx_http_xslt_filter_module);
if (systemId && !conf->external_entities) {
/*
* If external entiries in the internal DTD subset are disabled,
* we remove system identifiers from such entities. This makes sure
* that external entities cannot be used to directly request arbitrary
* files, but still can be used with public identifiers, assuming these
* are included into XML catalogs on the system.
*/
ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,
"xslt filter external entity ignored: "
"\"%s\" \"%s\" \"%s\"",
name ? name : (xmlChar *) "",
publicId ? publicId : (xmlChar *) "",
systemId ? systemId : (xmlChar *) "");
if (publicId) {
xmlSAX2EntityDecl(data, name, type, publicId, (xmlChar *) "",
content);
} else if (type == XML_EXTERNAL_GENERAL_PARSED_ENTITY) {
xmlSAX2EntityDecl(data, name, XML_INTERNAL_GENERAL_ENTITY,
NULL, NULL, (xmlChar *) "");
} else if (type == XML_EXTERNAL_PARAMETER_ENTITY) {
xmlSAX2EntityDecl(data, name, XML_INTERNAL_PARAMETER_ENTITY,
NULL, NULL, (xmlChar *) "");
}
return;
}
xmlSAX2EntityDecl(data, name, type, publicId, systemId, content);
}
static void ngx_cdecl
ngx_http_xslt_sax_error(void *data, const char *msg, ...)
{
@ -1096,6 +1171,7 @@ ngx_http_xslt_filter_create_conf(ngx_conf_t *cf)
*/
conf->last_modified = NGX_CONF_UNSET;
conf->external_entities = NGX_CONF_UNSET;
return conf;
}
@ -1128,6 +1204,7 @@ ngx_http_xslt_filter_merge_conf(ngx_conf_t *cf, void *parent, void *child)
}
ngx_conf_merge_value(conf->last_modified, prev->last_modified, 0);
ngx_conf_merge_value(conf->external_entities, prev->external_entities, 0);
return NGX_CONF_OK;
}
@ -1142,6 +1219,10 @@ ngx_http_xslt_filter_preconfiguration(ngx_conf_t *cf)
exsltRegisterAll();
#endif
#if (defined LIBXML_CATALOG_ENABLED && LIBXML_VERSION < 21400)
xmlCatalogSetDefaults(XML_CATA_ALLOW_GLOBAL);
#endif
return NGX_OK;
}