xref: /PHP-8.3/ext/mysqlnd/mysqlnd_loaddata.c (revision 01b3fc03)
1 /*
2   +----------------------------------------------------------------------+
3   | Copyright (c) The PHP Group                                          |
4   +----------------------------------------------------------------------+
5   | This source file is subject to version 3.01 of the PHP license,      |
6   | that is bundled with this package in the file LICENSE, and is        |
7   | available through the world-wide-web at the following url:           |
8   | https://www.php.net/license/3_01.txt                                 |
9   | If you did not receive a copy of the PHP license and are unable to   |
10   | obtain it through the world-wide-web, please send a note to          |
11   | license@php.net so we can mail you a copy immediately.               |
12   +----------------------------------------------------------------------+
13   | Authors: Andrey Hristov <andrey@php.net>                             |
14   |          Ulf Wendel <uw@php.net>                                     |
15   |          Georg Richter <georg@php.net>                               |
16   +----------------------------------------------------------------------+
17 */
18 #include "php.h"
19 #include "mysqlnd.h"
20 #include "mysqlnd_wireprotocol.h"
21 #include "mysqlnd_priv.h"
22 #include "mysqlnd_debug.h"
23 
24 /* {{{ mysqlnd_local_infile_init */
25 static
mysqlnd_local_infile_init(void ** ptr,const char * const filename)26 int mysqlnd_local_infile_init(void ** ptr, const char * const filename)
27 {
28 	MYSQLND_INFILE_INFO	*info;
29 	php_stream_context	*context = NULL;
30 
31 	DBG_ENTER("mysqlnd_local_infile_init");
32 
33 	info = ((MYSQLND_INFILE_INFO *)mnd_ecalloc(1, sizeof(MYSQLND_INFILE_INFO)));
34 	if (!info) {
35 		DBG_RETURN(1);
36 	}
37 
38 	*ptr = info;
39 
40 	/* check open_basedir */
41 	if (PG(open_basedir)) {
42 		if (php_check_open_basedir_ex(filename, 0) == -1) {
43 			strcpy(info->error_msg, "open_basedir restriction in effect. Unable to open file");
44 			info->error_no = CR_UNKNOWN_ERROR;
45 			DBG_RETURN(1);
46 		}
47 	}
48 
49 	info->filename = filename;
50 	info->fd = php_stream_open_wrapper_ex((char *)filename, "r", 0, NULL, context);
51 
52 	if (info->fd == NULL) {
53 		snprintf((char *)info->error_msg, sizeof(info->error_msg), "Can't find file '%-.64s'.", filename);
54 		info->error_no = MYSQLND_EE_FILENOTFOUND;
55 		DBG_RETURN(1);
56 	}
57 
58 	DBG_RETURN(0);
59 }
60 /* }}} */
61 
62 
63 /* {{{ mysqlnd_local_infile_read */
64 static
mysqlnd_local_infile_read(void * ptr,zend_uchar * buf,unsigned int buf_len)65 int mysqlnd_local_infile_read(void * ptr, zend_uchar * buf, unsigned int buf_len)
66 {
67 	MYSQLND_INFILE_INFO	*info = (MYSQLND_INFILE_INFO *)ptr;
68 	int count;
69 
70 	DBG_ENTER("mysqlnd_local_infile_read");
71 
72 	count = (int) php_stream_read(info->fd, (char *) buf, buf_len);
73 
74 	if (count < 0) {
75 		strcpy(info->error_msg, "Error reading file");
76 		info->error_no = CR_UNKNOWN_ERROR;
77 	}
78 
79 	DBG_RETURN(count);
80 }
81 /* }}} */
82 
83 
84 /* {{{ mysqlnd_local_infile_error */
85 static
mysqlnd_local_infile_error(void * ptr,char * error_buf,unsigned int error_buf_len)86 int	mysqlnd_local_infile_error(void * ptr, char *error_buf, unsigned int error_buf_len)
87 {
88 	MYSQLND_INFILE_INFO	*info = (MYSQLND_INFILE_INFO *)ptr;
89 
90 	DBG_ENTER("mysqlnd_local_infile_error");
91 
92 	if (info) {
93 		strlcpy(error_buf, info->error_msg, error_buf_len);
94 		DBG_INF_FMT("have info, %d", info->error_no);
95 		DBG_RETURN(info->error_no);
96 	}
97 
98 	strlcpy(error_buf, "Unknown error", error_buf_len);
99 	DBG_INF_FMT("no info, %d", CR_UNKNOWN_ERROR);
100 	DBG_RETURN(CR_UNKNOWN_ERROR);
101 }
102 /* }}} */
103 
104 
105 /* {{{ mysqlnd_local_infile_end */
106 static
mysqlnd_local_infile_end(void * ptr)107 void mysqlnd_local_infile_end(void * ptr)
108 {
109 	MYSQLND_INFILE_INFO	*info = (MYSQLND_INFILE_INFO *)ptr;
110 
111 	if (info) {
112 		/* php_stream_close segfaults on NULL */
113 		if (info->fd) {
114 			php_stream_close(info->fd);
115 			info->fd = NULL;
116 		}
117 		mnd_efree(info);
118 	}
119 }
120 /* }}} */
121 
122 
123 /* {{{ mysqlnd_local_infile_default */
124 PHPAPI void
mysqlnd_local_infile_default(MYSQLND_CONN_DATA * conn)125 mysqlnd_local_infile_default(MYSQLND_CONN_DATA * conn)
126 {
127 	conn->infile.local_infile_init = mysqlnd_local_infile_init;
128 	conn->infile.local_infile_read = mysqlnd_local_infile_read;
129 	conn->infile.local_infile_error = mysqlnd_local_infile_error;
130 	conn->infile.local_infile_end = mysqlnd_local_infile_end;
131 }
132 /* }}} */
133 
134 
135 static const char *lost_conn = "Lost connection to MySQL server during LOAD DATA of a local file";
136 
137 
138 /* {{{ mysqlnd_handle_local_infile */
139 enum_func_status
mysqlnd_handle_local_infile(MYSQLND_CONN_DATA * conn,const char * const filename,bool * is_warning)140 mysqlnd_handle_local_infile(MYSQLND_CONN_DATA * conn, const char * const filename, bool * is_warning)
141 {
142 	zend_uchar			*buf = NULL;
143 	zend_uchar			empty_packet[MYSQLND_HEADER_SIZE];
144 	enum_func_status	result = FAIL;
145 	unsigned int		buflen = 4096;
146 	void				*info = NULL;
147 	int					bufsize;
148 	size_t				ret;
149 	MYSQLND_INFILE		infile;
150 	MYSQLND_PFC			* net = conn->protocol_frame_codec;
151 	MYSQLND_VIO			* vio = conn->vio;
152 	bool				is_local_infile_enabled = (conn->options->flags & CLIENT_LOCAL_FILES) == CLIENT_LOCAL_FILES;
153 	const char*			local_infile_directory = conn->options->local_infile_directory;
154 	bool				is_local_infile_dir_set = local_infile_directory != NULL;
155 	bool				prerequisities_ok = TRUE;
156 
157 	DBG_ENTER("mysqlnd_handle_local_infile");
158 
159 	/*
160 		if local_infile is disabled, and local_infile_dir is not set, then operation is forbidden
161 	*/
162 	if (!is_local_infile_enabled && !is_local_infile_dir_set) {
163 		SET_CLIENT_ERROR(conn->error_info, CR_LOAD_DATA_LOCAL_INFILE_REJECTED, UNKNOWN_SQLSTATE,
164 						"LOAD DATA LOCAL INFILE is forbidden, check related settings like "
165 						"mysqli.allow_local_infile|mysqli.local_infile_directory or "
166 						"PDO::MYSQL_ATTR_LOCAL_INFILE|PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY");
167 		prerequisities_ok = FALSE;
168 	}
169 
170 	/*
171 		if local_infile_dir is set, then check whether it actually exists, and is accessible
172 	*/
173 	if (is_local_infile_dir_set) {
174 		php_stream *stream = php_stream_opendir(local_infile_directory, REPORT_ERRORS, NULL);
175 		if (stream) {
176 			php_stream_closedir(stream);
177 		} else {
178 			SET_CLIENT_ERROR(conn->error_info, CR_LOAD_DATA_LOCAL_INFILE_REJECTED, UNKNOWN_SQLSTATE, "cannot open local_infile_directory");
179 			prerequisities_ok = FALSE;
180 		}
181 	}
182 
183 	/*
184 		if local_infile is disabled and local_infile_dir is set, then we have to check whether
185 		filename is located inside its subtree
186 		but only in such a case, because when local_infile is enabled, then local_infile_dir is ignored
187 	*/
188 	if (prerequisities_ok && !is_local_infile_enabled && is_local_infile_dir_set) {
189 		if (php_check_specific_open_basedir(local_infile_directory, filename) == -1) {
190 			SET_CLIENT_ERROR(conn->error_info, CR_LOAD_DATA_LOCAL_INFILE_REJECTED, UNKNOWN_SQLSTATE,
191 							"LOAD DATA LOCAL INFILE DIRECTORY restriction in effect. Unable to open file");
192 			prerequisities_ok = FALSE;
193 		}
194 	}
195 
196 	if (!prerequisities_ok) {
197 		/* write empty packet to server */
198 		ret = net->data->m.send(net, vio, empty_packet, 0, conn->stats, conn->error_info);
199 		*is_warning = TRUE;
200 		goto infile_error;
201 	}
202 
203 	infile = conn->infile;
204 	/* allocate buffer for reading data */
205 	buf = (zend_uchar *) mnd_ecalloc(1, buflen);
206 
207 	*is_warning = FALSE;
208 
209 	/* init handler: allocate read buffer and open file */
210 	if (infile.local_infile_init(&info, (char *)filename)) {
211 		char tmp_buf[sizeof(conn->error_info->error)];
212 		int tmp_error_no;
213 		*is_warning = TRUE;
214 		/* error occurred */
215 		tmp_error_no = infile.local_infile_error(info, tmp_buf, sizeof(tmp_buf));
216 		SET_CLIENT_ERROR(conn->error_info, tmp_error_no, UNKNOWN_SQLSTATE, tmp_buf);
217 		/* write empty packet to server */
218 		ret = net->data->m.send(net, vio, empty_packet, 0, conn->stats, conn->error_info);
219 		goto infile_error;
220 	}
221 
222 	/* read data */
223 	while ((bufsize = infile.local_infile_read (info, buf + MYSQLND_HEADER_SIZE, buflen - MYSQLND_HEADER_SIZE)) > 0) {
224 		if ((ret = net->data->m.send(net, vio, buf, bufsize, conn->stats, conn->error_info)) == 0) {
225 			DBG_ERR_FMT("Error during read : %d %s %s", CR_SERVER_LOST, UNKNOWN_SQLSTATE, lost_conn);
226 			SET_CLIENT_ERROR(conn->error_info, CR_SERVER_LOST, UNKNOWN_SQLSTATE, lost_conn);
227 			goto infile_error;
228 		}
229 	}
230 
231 	/* send empty packet for eof */
232 	if ((ret = net->data->m.send(net, vio, empty_packet, 0, conn->stats, conn->error_info)) == 0) {
233 		SET_CLIENT_ERROR(conn->error_info, CR_SERVER_LOST, UNKNOWN_SQLSTATE, lost_conn);
234 		goto infile_error;
235 	}
236 
237 	/* error during read occurred */
238 	if (bufsize < 0) {
239 		char tmp_buf[sizeof(conn->error_info->error)];
240 		int tmp_error_no;
241 		*is_warning = TRUE;
242 		DBG_ERR_FMT("Bufsize < 0, warning,  %d %s %s", CR_SERVER_LOST, UNKNOWN_SQLSTATE, lost_conn);
243 		tmp_error_no = infile.local_infile_error(info, tmp_buf, sizeof(tmp_buf));
244 		SET_CLIENT_ERROR(conn->error_info, tmp_error_no, UNKNOWN_SQLSTATE, tmp_buf);
245 		goto infile_error;
246 	}
247 
248 	result = PASS;
249 
250 infile_error:
251 	/* get response from server and update upsert values */
252 	if (FAIL == conn->payload_decoder_factory->m.send_command_handle_response(
253 											conn->payload_decoder_factory,
254 											PROT_OK_PACKET, FALSE, COM_QUERY, FALSE,
255 											conn->error_info,
256 											conn->upsert_status,
257 											&conn->last_message)) {
258 		result = FAIL;
259 	}
260 
261 	(*conn->infile.local_infile_end)(info);
262 	if (buf) {
263 		mnd_efree(buf);
264 	}
265 	DBG_INF_FMT("%s", result == PASS? "PASS":"FAIL");
266 	DBG_RETURN(result);
267 }
268 /* }}} */
269