PluginProbe ʕ •ᴥ•ʔ
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments / 4.5.0
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments v4.5.0
4.5.1 4.5.0 4.4.2 4.4.1 4.4.0 4.3.3 4.3.2 4.3.1 4.3.0 4.2.3 4.2.2 4.2.1 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.11.0 1.11.1 1.11.2 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.9.0 1.9.1 1.9.2 1.9.3 1.9.4 1.9.5 2.0.0 2.0.1 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.10.0 2.10.1 2.11.0 2.11.1 2.11.2 2.11.3 2.11.4 2.12.0 2.13.0 2.14.0 2.14.1 2.15.0 2.15.1 2.16.0 2.16.1 2.16.2 2.16.3 2.17.0 2.17.1 2.17.2 2.18.0 2.19.0 2.19.2 2.19.3 2.19.4 2.2.0 2.2.1 2.20.0 2.20.1 2.20.2 2.20.3 2.20.4 2.20.5 2.20.6 2.21.0 2.22.0 2.22.1 2.23.0 2.24.0 2.25.0 2.25.1 2.25.2 2.26.0 2.27.0 2.27.1 2.28.0 2.29.0 2.29.1 2.29.2 2.29.3 2.29.4 2.3.0 2.3.1 2.30.0 2.31.0 2.31.1 2.31.2 2.31.3 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.40.0 2.40.1 2.5.0 2.5.1 2.5.2 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.9.0 3.0.0 3.0.0-RC1 3.0.0-RC2 3.0.0-beta1 3.0.0-beta2 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.10.0 3.10.1 3.11.0 3.12.0 3.13.0 3.13.1 3.13.2 3.13.3 3.13.4 3.14.0 3.15.0 3.15.1 3.15.2 3.15.3 3.15.4 3.15.5 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.16.5 3.16.6 3.16.7 3.16.8 3.17.0 3.17.1 3.17.2 3.17.3 3.17.4 3.17.5 3.17.6 3.18.0 3.19.0 3.19.1 3.19.2 3.2.0 3.2.1 3.2.2 3.20.0 3.20.1 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 4.0.0 4.0.1 4.0.2 4.0.3 trunk 4.1.0 0.2.19.1 4.1.1 1.0.0 4.2.0 1.0.1 1.0.2
surecart / app / src / Support / GitHubInstaller.php
surecart / app / src / Support Last commit date
Blocks 4 months ago Contracts 9 months ago Errors 1 month ago Scripts 3 days ago Arrays.php 3 years ago ColorService.php 3 years ago Currency.php 1 year ago Encryption.php 3 years ago GitHubInstaller.php 3 days ago Interval.php 7 months ago Server.php 2 years ago TimeDate.php 3 months ago Translations.php 3 years ago URL.php 2 years ago UtilityService.php 3 years ago UtilityServiceProvider.php 3 years ago kses.json 3 days ago
GitHubInstaller.php
161 lines
1 <?php
2
3 namespace SureCart\Support;
4
5 /**
6 * Validates GitHub-hosted plugin/theme zip URLs before they are used for installation.
7 *
8 * Acts as an allow-list gate so only trusted GitHub download hosts and installable
9 * paths are accepted. This is a structural pre-flight check, not a replacement for
10 * a runtime SSRF guard.
11 */
12 class GitHubInstaller {
13 /**
14 * Hosts that are permitted to serve installable zips.
15 *
16 * Matched exactly (lowercased) — no suffix or substring matching.
17 *
18 * @var array
19 */
20 protected static $allowed_hosts = array(
21 'github.com',
22 'codeload.github.com',
23 'objects.githubusercontent.com',
24 'release-assets.githubusercontent.com',
25 );
26
27 /**
28 * Validate a URL and return a trusted GitHub zip download URL.
29 *
30 * @param string $url The candidate download URL.
31 *
32 * @return string|\WP_Error The trimmed URL on success, or a WP_Error describing the failure.
33 */
34 public static function resolveZipUrl( string $url ) {
35 $url = trim( $url );
36
37 if ( empty( $url ) ) {
38 return new \WP_Error( 'sc_invalid_url', __( 'Please provide a download URL.', 'surecart' ) );
39 }
40
41 // Cheap pre-filter — catches obviously malformed input before deeper checks.
42 if ( ! wp_http_validate_url( $url ) ) {
43 return new \WP_Error( 'sc_invalid_url', __( 'The download URL is not valid.', 'surecart' ) );
44 }
45
46 $parts = wp_parse_url( $url );
47
48 if ( empty( $parts['scheme'] ) || 'https' !== $parts['scheme'] ) {
49 return new \WP_Error( 'sc_invalid_url', __( 'The download URL must use HTTPS.', 'surecart' ) );
50 }
51
52 // Reject non-standard ports.
53 if ( isset( $parts['port'] ) && 443 !== (int) $parts['port'] ) {
54 return new \WP_Error( 'sc_invalid_url', __( 'The download URL must not specify a custom port.', 'surecart' ) );
55 }
56
57 // Reject embedded credentials (e.g. https://x@github.com@evil/...).
58 if ( ! empty( $parts['user'] ) || ! empty( $parts['pass'] ) ) {
59 return new \WP_Error( 'sc_invalid_url', __( 'The download URL must not contain user credentials.', 'surecart' ) );
60 }
61
62 if ( empty( $parts['host'] ) ) {
63 return new \WP_Error( 'sc_invalid_host', __( 'The download URL host is not allowed.', 'surecart' ) );
64 }
65
66 $host = strtolower( $parts['host'] );
67
68 if ( ! self::isAllowedHost( $host ) ) {
69 return new \WP_Error( 'sc_invalid_host', __( 'Only GitHub download URLs are allowed.', 'surecart' ) );
70 }
71
72 $path = isset( $parts['path'] ) ? $parts['path'] : '';
73
74 if ( 'github.com' === $host ) {
75 $valid = self::isValidGitHubPath( $path );
76 if ( is_wp_error( $valid ) ) {
77 return $valid;
78 }
79 } else {
80 $valid = self::isValidArchivePath( $path );
81 if ( is_wp_error( $valid ) ) {
82 return $valid;
83 }
84 }
85
86 return $url;
87 }
88
89 /**
90 * Check whether a host is on the installable allow-list.
91 *
92 * Matched exactly (lowercased) — no suffix or substring matching. Exposed so
93 * the install pipeline can re-validate redirect targets against the same list.
94 *
95 * @param string $host The host to check.
96 *
97 * @return bool
98 */
99 public static function isAllowedHost( string $host ): bool {
100 return in_array( strtolower( $host ), self::$allowed_hosts, true );
101 }
102
103 /**
104 * Validate the path shape for a github.com download URL.
105 *
106 * Accepts release assets and any path ending in .zip; rejects bare repo URLs.
107 *
108 * @param string $path The URL path component.
109 *
110 * @return true|\WP_Error True when installable, otherwise a WP_Error.
111 */
112 protected static function isValidGitHubPath( string $path ) {
113 $segment = '[A-Za-z0-9._-]+';
114
115 // /{owner}/{repo}/releases/latest/download/{file}.zip
116 if ( preg_match( '#^/' . $segment . '/' . $segment . '/releases/latest/download/' . $segment . '\.zip$#', $path ) ) {
117 return true;
118 }
119
120 // /{owner}/{repo}/releases/download/{tag}/{file}.zip
121 if ( preg_match( '#^/' . $segment . '/' . $segment . '/releases/download/' . $segment . '/' . $segment . '\.zip$#', $path ) ) {
122 return true;
123 }
124
125 // Any other github.com path that explicitly points at a .zip file.
126 if ( '.zip' === strtolower( substr( $path, -4 ) ) ) {
127 return true;
128 }
129
130 return new \WP_Error(
131 'sc_not_a_zip',
132 __( 'Please provide a GitHub release .zip URL (for example, .../releases/latest/download/plugin.zip) rather than a repository URL.', 'surecart' )
133 );
134 }
135
136 /**
137 * Validate the path shape for githubusercontent.com and codeload.github.com URLs.
138 *
139 * Requires a .zip suffix, except for codeload archive paths which serve zips
140 * without an extension (e.g. /{owner}/{repo}/zip/refs/...).
141 *
142 * @param string $path The URL path component.
143 *
144 * @return true|\WP_Error True when installable, otherwise a WP_Error.
145 */
146 protected static function isValidArchivePath( string $path ) {
147 if ( '.zip' === strtolower( substr( $path, -4 ) ) ) {
148 return true;
149 }
150
151 $segment = '[A-Za-z0-9._-]+';
152
153 // codeload archive path: /{owner}/{repo}/zip/refs/...
154 if ( preg_match( '#^/' . $segment . '/' . $segment . '/zip/#', $path ) ) {
155 return true;
156 }
157
158 return new \WP_Error( 'sc_not_a_zip', __( 'The download URL must point to a .zip file.', 'surecart' ) );
159 }
160 }
161