fix: security audit — patch vulnerabilities V-02 through V-16
- V-02: XSS via innerHTML in profile.js — wrap err.message in escapeHtml() - V-03: IDOR upload to other users' folders — add folder ownership check - V-04: IDOR create folders in other users' trees — add parent ownership check - V-06: Content-Disposition header injection — RFC 5987 percent-encoding - V-08: WebDAV MOVE/COPY destination without ownership — add assert_owner checks - V-09: .gitignore missing cert/key patterns — add *.pem, *.key, *.p12, etc. - V-11: Username accepts XSS payloads — restrict to [a-zA-Z0-9._-] - V-12: Minimal email validation — reject forbidden chars, require domain dot - V-13: admin_reset_password doesn't invalidate sessions — revoke all sessions - V-14: Rate limiting bypassable via X-Forwarded-For — gate behind OXICLOUD_TRUST_PROXY_HEADERS - V-15: Cookie Secure flag off by default — default to true (safe-by-default) - V-16: LIKE wildcard injection in searches — add like_escape() helper across 9 sites
This commit is contained in:
@@ -274,7 +274,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
calendar_id: &Uuid,
|
||||
summary: &str,
|
||||
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
|
||||
let search_pattern = format!("%{}%", summary);
|
||||
let search_pattern = super::like_escape(summary);
|
||||
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
|
||||
@@ -277,7 +277,7 @@ impl ContactRepository for ContactPgRepository {
|
||||
}
|
||||
|
||||
async fn get_contacts_by_email(&self, email: &str) -> ContactRepositoryResult<Vec<Contact>> {
|
||||
let search_pattern = format!("%{}%", email);
|
||||
let search_pattern = super::like_escape(email);
|
||||
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
@@ -339,7 +339,7 @@ impl ContactRepository for ContactPgRepository {
|
||||
address_book_id: &Uuid,
|
||||
query: &str,
|
||||
) -> ContactRepositoryResult<Vec<Contact>> {
|
||||
let search_pattern = format!("%{}%", query);
|
||||
let search_pattern = super::like_escape(query);
|
||||
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
|
||||
@@ -736,7 +736,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
if let Some(name) = &criteria.name_contains
|
||||
&& name.len() >= 3
|
||||
{
|
||||
query = query.bind(format!("%{}%", name));
|
||||
query = query.bind(super::like_escape(name));
|
||||
}
|
||||
query = query.bind(limit).bind(offset);
|
||||
|
||||
@@ -895,7 +895,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
if let Some(name) = &criteria.name_contains
|
||||
&& name.len() >= 3
|
||||
{
|
||||
query = query.bind(format!("%{}%", name));
|
||||
query = query.bind(super::like_escape(name));
|
||||
}
|
||||
if let Some(types) = &criteria.file_types
|
||||
&& !types.is_empty()
|
||||
@@ -963,7 +963,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
query: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<File>, DomainError> {
|
||||
let pattern = format!("%{}%", query);
|
||||
let pattern = super::like_escape(query);
|
||||
let limit_i64 = limit as i64;
|
||||
|
||||
let rows: Vec<FileRow> = if let Some(fid) = folder_id {
|
||||
|
||||
@@ -739,7 +739,7 @@ impl FolderRepository for FolderDbRepository {
|
||||
} else {
|
||||
" AND fo.name ILIKE $3"
|
||||
},
|
||||
Some(format!("%{}%", name)),
|
||||
Some(super::like_escape(name)),
|
||||
),
|
||||
_ => ("", None),
|
||||
};
|
||||
@@ -861,7 +861,7 @@ impl FolderRepository for FolderDbRepository {
|
||||
user_id: &str,
|
||||
) -> Result<Vec<Folder>, DomainError> {
|
||||
let (where_extra, name_pattern) = match name_contains {
|
||||
Some(name) if name.len() >= 3 => (" AND fo.name ILIKE $3", Some(format!("%{}%", name))),
|
||||
Some(name) if name.len() >= 3 => (" AND fo.name ILIKE $3", Some(super::like_escape(name))),
|
||||
_ => ("", None),
|
||||
};
|
||||
|
||||
@@ -908,7 +908,7 @@ impl FolderRepository for FolderDbRepository {
|
||||
query: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<Folder>, DomainError> {
|
||||
let pattern = format!("%{}%", query);
|
||||
let pattern = super::like_escape(query);
|
||||
let limit_i64 = limit as i64;
|
||||
|
||||
let rows: Vec<FolderRow> = if let Some(pid) = parent_id {
|
||||
|
||||
@@ -38,3 +38,16 @@ pub use settings_pg_repository::SettingsPgRepository;
|
||||
pub use share_pg_repository::SharePgRepository;
|
||||
pub use trash_db_repository::TrashDbRepository;
|
||||
pub use user_pg_repository::UserPgRepository;
|
||||
|
||||
// ── SQL helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Escape SQL `LIKE` / `ILIKE` wildcard characters (`%` and `_`) in user
|
||||
/// input and wrap the result in `%…%` for a contains-match.
|
||||
///
|
||||
/// Without this, a user searching for `100%` would match *every* row because
|
||||
/// `%` is a wildcard in LIKE patterns.
|
||||
#[inline]
|
||||
pub fn like_escape(raw: &str) -> String {
|
||||
let escaped = raw.replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_");
|
||||
format!("%{escaped}%")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user