Repository navigation
Patch release for the 7.0 line. It stops the policy decoder from accepting malformed policy JSON and compiling it to something that matches more than was written. Exploiting this takes a policy author's mistake, not an outside attacker, but it failed open.
- What is now rejected. Decoding a policy, app permission or rate limit rule fails with
MalformedPolicyException(anIllegalArgumentException) whose message names the JSON path, e.g.Malformed JSON at $.statements[0]: unknown field [conditon] in a statement:- unknown fields in a policy, statement, app permission, permission group, permission or rate limit rule.
$schema(policy, app permission, rate limit rule) andspaced(permission group) stay accepted and ignored. Example:"conditon": {...}used to be ignored, which made the statement unconditional; - condition or action objects without exactly one key. Example:
{"all": {}, "inRole": {...}}used to compile toall; - scalars where a list belongs:
bool'sand/or,in'svalue,time'sdaysOfWeek,composite,statements,groups,permissions. Example:{"bool": {"and": "inRole"}}used to be an emptyand, which matches everything; - duplicate keys in any object, where the last one used to win silently.
- unknown fields in a policy, statement, app permission, permission group, permission or rate limit rule.
- Fail closed. A decode error never skips a policy, so it cannot drop a deny. A malformed local policy, rate limit or shadow file stops startup with
Failed to load policy [<file>]: Malformed JSON at .... A malformed value read from Redis fails the requests that need it with500.
Compatibility
- Policies with typos, extra keys or scalar lists will now fail to load or fail the request instead of silently matching more. No example or test policy shipped with CoSec is affected.
- Policies already stored in Redis. 7.0.1 stored each policy as it had decoded it, so:
- typos and extra keys were dropped before storing. The stored copy reads fine on 7.0.2, but it keeps the widened meaning until you republish it from a fixed source. Only your source documents show the mistake;
- scalar
and/or/invalue/daysOfWeekwere stored as written. After the upgrade, every request that needs such a policy gets500: for a global policy that is every request. Withlocal-policy.init-repositoryandforce-refresh=false, startup fails withInit Policy - [<id>] failed.. Republishing the fixed policy withsetPolicyreplaces the value without reading it.
Before upgrading, find affected policies
-
Check the files you publish from with this
jqscript. It prints one line per problem 7.0.2 rejects:for f in src/main/resources/cosec-policy/*.json; do jq -r -f cosec-policy-lint.jq "$f" | sed "s|^|$f: |"; done
-
Check what Redis holds. Each stored value is a binary header followed by the JSON. With the default key prefix
cosec:redis-cli --scan --pattern 'cosec:policy:*' | grep -v ':policy:history:' | while read -r key; do redis-cli --raw GET "$key" | LC_ALL=C sed '1s/^[^{]*//' | grep '^{' | jq -r -f cosec-policy-lint.jq | sed "s|^|$key: |" done
Repeat with
cosec:app:permission:*. Only scalar lists can show up here. -
Fix each finding at its source, and republish with
setPolicy. 7.0.1 reads the fixed version too, so you can do this before upgrading. Also republish the policies whose source had a typo or an extra key.
The authoritative check is loading your policy files with 7.0.2 in a unit test.
cosec-policy-lint.jq
def keys_not_in($allowed; $path):
keys[] | select(. as $k | $allowed | index($k) | not) | "\($path): unknown field \(.)";
def condition($path):
if type != "object" then "\($path): a condition must be an object"
elif length != 1 then "\($path): a condition must have exactly one key, has \(keys)"
else to_entries[0] as $e
| if $e.key == "bool" and ($e.value | type) == "object" then
$e.value | (
(to_entries[] | select(.key == "and" or .key == "or") as $l
| if ($l.value | type) != "array" then "\($path).bool.\($l.key): must be an array"
else $l.value | to_entries[] | .key as $i | .value | condition("\($path).bool.\($l.key)[\($i)]") end))
elif $e.key == "in" and ($e.value.value | type) != "array" then "\($path).in.value: must be an array"
elif $e.key == "time" and ($e.value | has("daysOfWeek")) and ($e.value.daysOfWeek | type) != "array" then
"\($path).time.daysOfWeek: must be an array"
else empty end
end;
def action($path):
if type == "string" then empty
elif type == "array" then to_entries[] | .key as $i | .value | action("\($path)[\($i)]")
elif type == "object" then
if length != 1 then "\($path): an action must have exactly one key, has \(keys)"
elif has("composite") then
if (.composite | type) != "array" then "\($path).composite: must be an array"
else .composite | to_entries[] | .key as $i | .value | action("\($path).composite[\($i)]") end
else empty end
else "\($path): an action must be a string, an array or an object" end;
def statement($path; $allowed):
keys_not_in($allowed; $path),
(if has("action") then .action | action("\($path).action") else empty end),
(if has("condition") then .condition | condition("\($path).condition") else empty end);
def items($path):
if type != "array" then {problem: "\($path): must be an array"}
else to_entries[] | {path: "\($path)[\(.key)]", value: .value} end;
def document:
def rule($p): statement($p; ["$schema","id","action","condition","key","permitsPerSecond","windowSeconds","strictFailure"]);
if type == "array" then to_entries[] | .key as $i | .value | rule("$[\($i)]")
elif has("groups") then
keys_not_in(["$schema","id","condition","groups"]; "$"),
(if has("condition") then .condition | condition("$.condition") else empty end),
(.groups | items("$.groups") | if has("problem") then .problem else .path as $g | .value
| keys_not_in(["name","description","permissions","spaced"]; $g),
(if has("permissions") then .permissions | items("\($g).permissions")
| if has("problem") then .problem
else .path as $pp | .value | statement($pp; ["id","name","description","effect","action","condition"]) end
else empty end) end)
elif has("permitsPerSecond") then rule("$")
else
keys_not_in(["$schema","id","name","category","description","type","tenantId","condition","statements"]; "$"),
(if has("condition") then .condition | condition("$.condition") else empty end),
(if has("statements") then .statements | items("$.statements")
| if has("problem") then .problem else .path as $sp | .value | statement($sp; ["name","effect","action","condition"]) end
else empty end)
end;
document
See "7.0.1 to 7.0.2" in the upgrading guide.
implementation("me.ahoo.cosec:cosec-spring-boot-starter:7.0.2")