Your APEX login state can be set from the URL, unless you restrict it
Fix Oracle APEX Tested on APEX 26.1.0securitysession state protectionapplication itemspublic app
This one is about security, so I’ll give the short version first. If your APEX application keeps “who is logged in” in application items, check each item’s Session State Protection. When it says Unrestricted, anyone can set the value from a URL, and every page that trusts it trusts the visitor.
I found this in a public application: No Authentication at the APEX level, with its own login page and its own user table. It’s a common design for customer portals. By putting a user id in the URL, a visitor who had never logged in opened another user’s results page and downloaded their PDF report.
How the application worked#
The login page checked the username and password against the application’s own table. If they matched, a process stored the identity in application items:
-- the login process, after the password check
:G_LOGGED_IN := 'Y';
:G_USER_ID := l_user_id;
:G_USER_ROLE := l_role;
Every protected page began with a guard:
-- page guard (a Before Header branch / process)
if nvl(:G_LOGGED_IN, 'N') <> 'Y' then
-- redirect to the login page
end if;
and the reports filtered by :G_USER_ID. That is a reasonable design. It has one hidden assumption: that G_LOGGED_IN and G_USER_ID can only be set by the login process.
The attack#
APEX URLs can set item values, and that includes application items. The classic f?p syntax has an item-names part and an item-values part:
f?p=APP:PAGE:SESSION:REQUEST:DEBUG:CLEARCACHE:ITEM_NAMES:ITEM_VALUES
So I opened the home page as an anonymous visitor to get a session, and then requested:
f?p=100:10:<session>::NO::G_LOGGED_IN,G_USER_ID:Y,42
APEX set both items, the page guard saw G_LOGGED_IN = 'Y', and page 10 showed user 42’s results. The download link on the page returned user 42’s PDF report. No password was ever sent.
The cause is the application items’ protection level. They were Unrestricted, which is what you get when nobody changes it. For an item that only a server-side process should ever set, that is the wrong setting.
The fix: Restricted, “may not be set from browser”#
For each identity item:
Shared Components → Application Items → (the item) → Security → Session State Protection = Restricted - May not be set from browser
This blocks every attempt to set the item from the browser: URL parameters, form posts and Ajax. Server-side code is not affected. Your login process can still assign :G_USER_ID := ... or call apex_util.set_session_state, because that runs on the server. That is exactly the split you want: only the login process can say who the user is.
The attack after the change:
| Attempt | Before | After |
|---|---|---|
| Set the identity items in the URL, open the user’s page | Another user’s results, no login | Session state protection violation, no data |
| Download that user’s report in the same session | The full PDF | Refused (403) |
| Open the page directly | (not tried) | Redirected to the login page |
Check one application-level setting as well: Shared Components → Security Attributes → Session State Protection must be Enabled, which is the default for new applications. The item settings rely on it.
Find the exposed items#
This query lists the application items and their protection level. N is Unrestricted:
select item_name, session_state_protection
from apex_application_items
where application_id = :APP_ID
order by item_name;
Any item that holds identity, a role, a tenant or customer id, or a “logged in” flag should not be N. The same idea applies to page items that the user should never change (the column is ITEM_PROTECTION_LEVEL in APEX_APPLICATION_PAGE_ITEMS). For page items passed in links, the Checksum Required levels are the usual answer, which is a topic for another post.
Why this matters more in public applications#
With APEX authentication, :APP_USER is set by APEX from a real login, and a URL can’t change it. The risk appears when an application builds its own identity out of ordinary items: public portals with a custom login, or authenticated apps that keep a role or a tenant id in an application item “for convenience”. The item is only as trustworthy as its protection level.
Checklist for an application that stores identity in items:
- Every identity, role or “logged in” item: Restricted - May not be set from browser.
- Session State Protection enabled for the application.
- Only one server-side place sets those items: the login process, after the password check. Search the application for any other assignment (processes, computations, dynamic actions) and remove it.
- Page guards fail closed:
nvl(:G_LOGGED_IN, 'N') <> 'Y', so an empty value means “not logged in”. - Downloads and Ajax callbacks check ownership themselves. They don’t rely on the page that shows the link.
- Test it the way an attacker would: open a fresh anonymous session and try to set the items from the URL.