Skip to content

Commit 9a7fee5

Browse files
fix: secure AI Builder data endpoints (#1337)
1 parent 41c7ec5 commit 9a7fee5

2 files changed

Lines changed: 112 additions & 11 deletions

File tree

‎classes/Visualizer/Module/AIBuilder.php‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,17 @@ private function _verify_create_nonce(): void {
9494
}
9595
}
9696

97+
/**
98+
* Verify that the current user can edit a chart.
99+
*
100+
* @param int $chart_id Chart ID.
101+
*/
102+
private function _verify_chart_access( $chart_id ): void {
103+
if ( ! current_user_can( 'edit_post', $chart_id ) ) {
104+
wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'visualizer' ) ), 403 );
105+
}
106+
}
107+
97108
/**
98109
* Persist chart data + series from a source.
99110
*
@@ -159,6 +170,7 @@ public function getChartNonce(): void {
159170
if ( ! $chart_id || ! get_post( $chart_id ) ) {
160171
wp_send_json_error( array( 'message' => __( 'Chart not found.', 'visualizer' ) ) );
161172
}
173+
$this->_verify_chart_access( $chart_id );
162174
wp_send_json_success(
163175
array(
164176
'upload_nonce' => wp_create_nonce( 'visualizer-ai-upload-' . $chart_id ),
@@ -180,6 +192,7 @@ public function fetchChart(): void {
180192
if ( ! $chart || $chart->post_type !== Visualizer_Plugin::CPT_VISUALIZER ) {
181193
wp_send_json_error( array( 'message' => __( 'Chart not found.', 'visualizer' ) ) );
182194
}
195+
$this->_verify_chart_access( $chart_id );
183196

184197
$series = get_post_meta( $chart_id, Visualizer_Plugin::CF_SERIES, true );
185198
$data = Visualizer_Module::get_chart_data( $chart, '', false );
@@ -259,6 +272,7 @@ public function uploadData(): void {
259272
if ( ! get_post( $chart_id ) ) {
260273
wp_send_json_error( array( 'message' => __( 'Chart not found.', 'visualizer' ) ) );
261274
}
275+
$this->_verify_chart_access( $chart_id );
262276

263277
$source_type = isset( $_POST['source_type'] ) ? sanitize_key( $_POST['source_type'] ) : 'csv_string';
264278
$source = null;
@@ -298,17 +312,6 @@ public function uploadData(): void {
298312
}
299313
$url = wp_unslash( $_POST['file_url'] );
300314

301-
// Allow local absolute paths in dev (same CSVs used by Classic).
302-
if ( is_string( $url ) && file_exists( $url ) && is_readable( $url ) ) {
303-
$ext = strtolower( pathinfo( $url, PATHINFO_EXTENSION ) );
304-
if ( 'xlsx' === $ext && class_exists( 'Visualizer_Source_Xlsx' ) ) {
305-
$source = new Visualizer_Source_Xlsx( $url );
306-
} else {
307-
$source = new Visualizer_Source_Csv( $url );
308-
}
309-
break;
310-
}
311-
312315
if ( function_exists( 'wp_http_validate_url' ) ) {
313316
$validated_url = wp_http_validate_url( (string) $url );
314317
$url = false === $validated_url ? false : (string) $validated_url;
@@ -436,6 +439,7 @@ public function generateChart(): void {
436439
if ( ! $chart_id || ! get_post( $chart_id ) ) {
437440
wp_send_json_error( array( 'message' => __( 'Chart not found.', 'visualizer' ) ) );
438441
}
442+
$this->_verify_chart_access( $chart_id );
439443

440444
$prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : '';
441445
$series = isset( $_POST['series'] ) ? wp_unslash( $_POST['series'] ) : '';
@@ -561,6 +565,7 @@ public function saveChart(): void {
561565
if ( ! $chart_id || ! get_post( $chart_id ) ) {
562566
wp_send_json_error( array( 'message' => __( 'Chart not found.', 'visualizer' ) ) );
563567
}
568+
$this->_verify_chart_access( $chart_id );
564569
if ( empty( $code ) ) {
565570
wp_send_json_error( array( 'message' => __( 'No chart code found. Generate a chart first.', 'visualizer' ) ) );
566571
}

‎tests/test-ajax.php‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,102 @@ public function setUp(): void {
6060

6161
}
6262

63+
/**
64+
* Test that the AI Builder URL import rejects local file paths.
65+
*/
66+
public function test_ai_builder_file_url_rejects_local_path() {
67+
wp_set_current_user( $this->contibutor_user_id );
68+
$chart_id = $this->factory->post->create(
69+
array(
70+
'post_type' => Visualizer_Plugin::CPT_VISUALIZER,
71+
'post_status' => 'draft',
72+
'post_author' => $this->contibutor_user_id,
73+
)
74+
);
75+
$file = wp_tempnam( 'visualizer-local.csv' );
76+
file_put_contents( $file, "Label,Value\nstring,number\nSecret,42" );
77+
78+
$_POST = array(
79+
'chart_id' => $chart_id,
80+
'nonce' => wp_create_nonce( 'visualizer-ai-upload-' . $chart_id ),
81+
'source_type' => 'file_url',
82+
'file_url' => $file,
83+
);
84+
85+
try {
86+
$this->_handleAjax( 'visualizer-ai-upload' );
87+
} catch ( WPAjaxDieContinueException $e ) {
88+
// Expected after wp_send_json_error().
89+
}
90+
wp_delete_file( $file );
91+
92+
$response = json_decode( $this->_last_response );
93+
$this->assertFalse( $response->success );
94+
$this->assertSame( 'Invalid URL. Please check the URL and try again.', $response->data->message );
95+
}
96+
97+
/**
98+
* Test that a user cannot request an upload nonce for another user's chart.
99+
*/
100+
public function test_ai_builder_chart_nonce_requires_chart_edit_permission() {
101+
$chart_id = $this->factory->post->create(
102+
array(
103+
'post_type' => Visualizer_Plugin::CPT_VISUALIZER,
104+
'post_status' => 'publish',
105+
'post_author' => $this->admin_user_id,
106+
)
107+
);
108+
wp_set_current_user( $this->contibutor_user_id );
109+
110+
$_POST = array(
111+
'chart_id' => $chart_id,
112+
'nonce' => wp_create_nonce( 'visualizer-ai-builder' ),
113+
);
114+
115+
try {
116+
$this->_handleAjax( 'visualizer-ai-chart-nonce' );
117+
} catch ( WPAjaxDieContinueException $e ) {
118+
// Expected after wp_send_json_error().
119+
}
120+
121+
$response = json_decode( $this->_last_response );
122+
$this->assertFalse( $response->success );
123+
$this->assertSame( 'Unauthorized.', $response->data->message );
124+
}
125+
126+
/**
127+
* Test that a user cannot upload data to another user's chart.
128+
*/
129+
public function test_ai_builder_upload_requires_chart_edit_permission() {
130+
$chart_id = $this->factory->post->create(
131+
array(
132+
'post_type' => Visualizer_Plugin::CPT_VISUALIZER,
133+
'post_status' => 'publish',
134+
'post_author' => $this->admin_user_id,
135+
)
136+
);
137+
$original_content = get_post_field( 'post_content', $chart_id );
138+
wp_set_current_user( $this->contibutor_user_id );
139+
140+
$_POST = array(
141+
'chart_id' => $chart_id,
142+
'nonce' => wp_create_nonce( 'visualizer-ai-upload-' . $chart_id ),
143+
'source_type' => 'csv_string',
144+
'csv_data' => "Label,Value\nstring,number\nSecret,42",
145+
);
146+
147+
try {
148+
$this->_handleAjax( 'visualizer-ai-upload' );
149+
} catch ( WPAjaxDieContinueException $e ) {
150+
// Expected after wp_send_json_error().
151+
}
152+
153+
$response = json_decode( $this->_last_response );
154+
$this->assertFalse( $response->success );
155+
$this->assertSame( 'Unauthorized.', $response->data->message );
156+
$this->assertSame( $original_content, get_post_field( 'post_content', $chart_id ) );
157+
}
158+
63159
/**
64160
* Test the AJAX response for fetching the database data.
65161
*/

0 commit comments

Comments
 (0)