Skip to content

Commit 031e2c4

Browse files
committed
SECURITY: fixes users being able to re-run migrations
1 parent 3d02cdc commit 031e2c4

File tree

7 files changed

+32
-6
lines changed

7 files changed

+32
-6
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# CHANGELOG.md
22

3+
## 0.19.1 (2024-02-28)
4+
- **SECURITY**: fixes users being able to re-run migrations by visiting `/sqlpage/migrations/NNNN_name.sql` pages. If you are using sqlpage migrations, your migrations are not idempotent, and you use the default SQLPAGE_WEB_ROOT (`./`) and `SQLPAGE_CONFIGURATION_DIRECTORY` (`./sqlpage/`), you should upgrade to this version as soon as possible. If you are using a custom `SQLPAGE_WEB_ROOT` or `SQLPAGE_CONFIGURATION_DIRECTORY` or your migrations are idempotent, you can upgrade at your convenience.
5+
36
## 0.19.0 (2024-02-25)
47

58
- Updated the chart component to use the latest version of the charting library

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sqlpage"
3-
version = "0.19.0"
3+
version = "0.19.1"
44
edition = "2021"
55
description = "A SQL-only web application framework. Takes .sql files and formats the query result using pre-made configurable professional-looking components."
66
keywords = ["web", "sql", "framework"]

configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Here are the available configuration options and their default values:
1818
| `database_connection_acquire_timeout_seconds` | 10 | How long to wait when acquiring a database connection from the pool before giving up and returning an error. |
1919
| `sqlite_extensions` | | An array of SQLite extensions to load, such as `mod_spatialite` |
2020
| `web_root` | `.` | The root directory of the web server, where the `index.sql` file is located. |
21-
| `configuration_directory` | `./sqlpage/` | The directory where the `sqlpage.json` file is located. This is used to find the path to `templates/`, `migrations/`, and `on_connect.sql`. Obviously, this configuration parameter can be set only through environment variables, not through the `sqlpage.json` file itself in order to find the `sqlpage.json` file. |
21+
| `configuration_directory` | `./sqlpage/` | The directory where the `sqlpage.json` file is located. This is used to find the path to `templates/`, `migrations/`, and `on_connect.sql`. Obviously, this configuration parameter can be set only through environment variables, not through the `sqlpage.json` file itself in order to find the `sqlpage.json` file. Be careful not to use a path that is accessible from the public WEB_ROOT |
2222
| `allow_exec` | false | Allow usage of the `sqlpage.exec` function. Do this only if all users with write access to sqlpage query files and to the optional `sqlpage_files` table on the database are trusted. |
2323
| `max_uploaded_file_size` | 5242880 | Maximum size of uploaded files in bytes. Defaults to 5 MiB. |
2424
| `https_domain` | | Domain name to request a certificate for. Setting this parameter will automatically make SQLPage listen on port 443 and request an SSL certificate. The server will take a little bit longer to start the first time it has to request a certificate. |

src/file_cache.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,16 @@ impl<T: AsyncFromStrWithState> FileCache<T> {
9595
self.static_files.insert(path, Cached::new(contents));
9696
}
9797

98+
/// Gets a file from the cache, or loads it from the file system if it's not there
99+
/// This is a privileged operation; it should not be used for user-provided paths
98100
pub async fn get(&self, app_state: &AppState, path: &PathBuf) -> anyhow::Result<Arc<T>> {
101+
self.get_with_privilege(app_state, path, true).await
102+
}
103+
104+
/// Gets a file from the cache, or loads it from the file system if it's not there
105+
/// The privileged parameter is used to determine whether the access should be denied
106+
/// if the file is in the sqlpage/ config directory
107+
pub async fn get_with_privilege(&self, app_state: &AppState, path: &PathBuf, privileged: bool) -> anyhow::Result<Arc<T>> {
99108
log::trace!("Attempting to get from cache {:?}", path);
100109
if let Some(cached) = self.cache.read().await.get(path) {
101110
if app_state.config.environment.is_prod() && !cached.needs_check() {
@@ -104,7 +113,7 @@ impl<T: AsyncFromStrWithState> FileCache<T> {
104113
}
105114
match app_state
106115
.file_system
107-
.modified_since(app_state, path, cached.last_check_time(), true)
116+
.modified_since(app_state, path, cached.last_check_time(), privileged)
108117
.await
109118
{
110119
Ok(false) => {
@@ -120,7 +129,7 @@ impl<T: AsyncFromStrWithState> FileCache<T> {
120129
log::trace!("Loading and parsing {:?}", path);
121130
let file_contents = app_state
122131
.file_system
123-
.read_to_string(app_state, path, true)
132+
.read_to_string(app_state, path, privileged)
124133
.await;
125134

126135
let parsed = match file_contents {

src/webserver/http.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ async fn process_sql_request(
360360
let app_state: &web::Data<AppState> = req.app_data().expect("app_state");
361361
let sql_file = app_state
362362
.sql_file_cache
363-
.get(app_state, &sql_path)
363+
.get_with_privilege(app_state, &sql_path, false)
364364
.await
365365
.with_context(|| format!("Unable to get SQL file {sql_path:?}"))
366366
.map_err(anyhow_err_to_actix)?;

tests/index.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,20 @@ async fn test_csv_upload() -> actix_web::Result<()> {
184184
Ok(())
185185
}
186186

187+
#[actix_web::test]
188+
/// `/sqlpage/migrations/0001_init.sql` should return a 403 Forbidden
189+
async fn privileged_paths_are_not_accessible() {
190+
let resp_result = req_path("/sqlpage/migrations/0001_init.sql").await;
191+
assert!(resp_result.is_err(), "Accessing a migration file should be forbidden");
192+
let resp = resp_result.unwrap_err().error_response();
193+
assert_eq!(resp.status(), http::StatusCode::FORBIDDEN);
194+
assert!(
195+
String::from_utf8_lossy(&resp.into_body().try_into_bytes().unwrap())
196+
.to_lowercase()
197+
.contains("forbidden"),
198+
);
199+
}
200+
187201
async fn get_request_to(path: &str) -> actix_web::Result<TestRequest> {
188202
let data = make_app_data().await;
189203
Ok(test::TestRequest::get().uri(path).app_data(data))

0 commit comments

Comments
 (0)