PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.6.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.6.1
1.6.1 1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
← All changes | inc/pdf/pdf-utils.php +88 -2 1.3.0 → 1.6.1 View file →
@@ -71,9 +71,25 @@
71 71
72 72 /**
73 73 * Ensure the receipts directory exists with security files.
74 74 *
75 - * @return bool True if directory exists or was created.
75 + * The denial files only bind where the server reads them: .htaccess on
76 + * Apache and LiteSpeed, web.config on IIS. nginx and Caddy read neither, so
77 + * there the unguessable 128-bit filename is the only control — which is why
78 + * the generator proceeds regardless of what this returns. A host that
79 + * cannot write a denial file should not lose the ability to issue receipts;
80 + * refusing would trade a real feature for a control those servers were
81 + * never going to enforce anyway.
82 + *
83 + * There is no receipt download route in this plugin. Delivery — email
84 + * attachment and authenticated streaming download alike — lives entirely
85 + * in Pro; an earlier version of this note claimed a REST streaming
86 + * endpoint here, and none exists.
87 + *
88 + * @return bool True when the directory exists *and* every denial file this
89 + * method writes is present. Callers that need to know whether
90 + * the directory is actually hardened can act on it; the
91 + * generator deliberately does not, for the reason above.
76 92 * @since 1.0.0
77 93 */
78 94 public static function ensure_receipts_dir() {
79 95 $dir = self::get_receipts_dir();
@@ -81,8 +97,18 @@
81 97 if ( ! file_exists( $dir ) ) {
82 98 wp_mkdir_p( $dir );
83 99 }
84 100
101 + // Bail before writing if the directory is not there. Every write below
102 + // is unconditional on the directory existing, so on a host where
103 + // wp_mkdir_p() fails — an unwritable uploads path, an open_basedir
104 + // restriction — each one emitted a PHP warning into the log before
105 + // failing anyway. Nothing downstream is worse off: the caller already
106 + // treats a false return as "not hardened".
107 + if ( ! is_dir( $dir ) ) {
108 + return false;
109 + }
110 +
85 111 // Add .htaccess to prevent direct access.
86 112 $htaccess = $dir . '/.htaccess';
87 113 if ( ! file_exists( $htaccess ) ) {
88 114 file_put_contents( $htaccess, "deny from all\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
@@ -93,9 +119,69 @@
93 119 if ( ! file_exists( $index ) ) {
94 120 file_put_contents( $index, "<?php\n// Silence is golden.\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
95 121 }
96 122
97 - return is_dir( $dir );
123 + // IIS reads neither .htaccess nor index.php as an access rule, so it
124 + // needs its own denial. Written unconditionally rather than detected:
125 + // the file is inert on Apache/nginx/LiteSpeed, and detecting the server
126 + // from $_SERVER['SERVER_SOFTWARE'] is unreliable behind a proxy.
127 + //
128 + // requestFiltering under system.webServer/security, matching SureMail's
129 + // attachments directory — minus its <handlers><remove> block. That block
130 + // is belt-and-braces on top of two controls already here, and IIS raises
131 + // a configuration error when <remove> names a handler the site does not
132 + // have (the names are host-specific — SureMail hardcodes
133 + // 'php-7.4.33'). Shipping something that can itself 500 the directory
134 + // is the exact failure this rewrite exists to remove, and it cannot be
135 + // tested from here, so it is left out: hiddenSegments already refuses
136 + // every request to the path, and fileExtensions already refuses
137 + // executables.
138 + //
139 + // The first attempt used
140 + // system.webServer/authorization, which is not a real section — the
141 + // ASP.NET element misplaced. IIS answers 500.19 on an unrecognised
142 + // section, so the directory ended up "protected" by a configuration
143 + // error rather than by the control this claims, and would have opened
144 + // the moment somebody fixed the 500. requestFiltering also needs no
145 + // extra role service, where URL Authorization 500s unless it is
146 + // installed.
147 + $web_config = $dir . '/web.config';
148 + if ( ! file_exists( $web_config ) ) {
149 + $config = '<?xml version="1.0" encoding="UTF-8"?>' . "\n"
150 + . '<configuration>' . "\n"
151 + . ' <system.webServer>' . "\n"
152 + . ' <security>' . "\n"
153 + . ' <requestFiltering>' . "\n"
154 + . ' <hiddenSegments>' . "\n"
155 + . ' <add segment="receipts" />' . "\n"
156 + . ' </hiddenSegments>' . "\n"
157 + . ' <fileExtensions allowUnlisted="true">' . "\n"
158 + . ' <add fileExtension=".php" allowed="false" />' . "\n"
159 + . ' <add fileExtension=".phtml" allowed="false" />' . "\n"
160 + . ' <add fileExtension=".php3" allowed="false" />' . "\n"
161 + . ' <add fileExtension=".php4" allowed="false" />' . "\n"
162 + . ' <add fileExtension=".php5" allowed="false" />' . "\n"
163 + . ' <add fileExtension=".php7" allowed="false" />' . "\n"
164 + . ' <add fileExtension=".phar" allowed="false" />' . "\n"
165 + . ' <add fileExtension=".phps" allowed="false" />' . "\n"
166 + . ' <add fileExtension=".pht" allowed="false" />' . "\n"
167 + . ' <add fileExtension=".phpt" allowed="false" />' . "\n"
168 + . ' <add fileExtension=".inc" allowed="false" />' . "\n"
169 + . ' </fileExtensions>' . "\n"
170 + . ' </requestFiltering>' . "\n"
171 + . ' </security>' . "\n"
172 + . ' </system.webServer>' . "\n"
173 + . '</configuration>' . "\n";
174 + file_put_contents( $web_config, $config ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
175 + }
176 +
177 + // The directory has to exist *and* be protected. Returning only
178 + // is_dir() reported success on a directory whose hardening writes had
179 + // silently failed — an open receipts directory holding donor names,
180 + // addresses and amounts, with the caller told everything was fine.
181 + // web.config included: it was left out, so on IIS — the one platform it
182 + // exists for — a failed write still reported success.
183 + return is_dir( $dir ) && file_exists( $htaccess ) && file_exists( $index ) && file_exists( $web_config );
98 184 }
99 185
100 186 /**
101 187 * Get the temp directory for mPDF.