One Cloud Please

Cedar: Avoiding the cracks

06 July 2023

With the open-source release of the Cedar engine and the general availability release of Amazon Verified Permissions, more and more engineers are considering integrating Cedar into their own systems for authorization, but what do policy authors need to consider to avoid unexpected outcomes?

In this post, I’ll walk through my experiences in where policy authoring can go wrong and the steps you can take to overcome these issues. This post will walk through some advanced evaluation scenarios, so if you’re new to the Cedar language I highly recommend you first read my introductory post on the topic, Cedar: A new policy language.

Non-unique entity identifiers

Though I mentioned it in my previous post, it’s important to always use unique identifiers for entities to ensure they do not get re-used in the future. The reason this may be a problem is that a reliance may start to occur on the entity, the entity goes away at some point in time, then a new entity of the same name comes into existence at a later point.

For example, consider the following statement:

permit(
    principal == User::"John",
    action,
    resource == Account::"Corporate"
);

If the user named John leaves the company, and then another John joins the company and happens to take the same entity identifier, it’s possible for the new John to inherit some privileges he should not be entitled to. The Cedarland blog has some more detail on the reasoning behind this.

Solutions

Always use unique identifiers, such as the identifiers your IdP provider uses, to uniquely identify principals. Additionally, use resource identifiers which are also unique for the context provided. Comments and annotations can help you keep track of identifiers where necessary.

permit(
    principal == User::"9a6afab1-5a37-4c90-aa40-24277b93ca28", // John Smith
    action,
    resource == Account::"710f18bc-b8ab-4313-b362-8e6264cfcf91" // Corporate Account
);

Invalid statements

Invalid statements not being evaluated is in my opinion one of the easiest ways to get an unexpected result from your policy evaluations. Consider the following policy:

permit(
    principal,
    action == Action::"Connect",
    resource
);

forbid(
    principal,
    action == Action::"Connect",
    resource == Endpoint::"AdminEndpoint"
) unless {
    context.viaAdminNetwork == true
};

The intention behind the policy is to allow connections to all endpoints except the admin endpoint unless the context object has the viaAdminNetwork key set to true. Unfortunately, the implementation of the context object in this example is that the viaAdminNetwork key is omitted, not false, if the call does not come from the admin network.

The result of this is that the forbid statement is not processed as there is an evaluation error due to the missing key. However, as the permit statement has been evaluated, and there are no other valid forbid statements, the result is an allow of the call. Even though the evaluated result is allow, there will be errors in the diagnostic return, as you can see from this Cedar playground screenshot:

There is more discussion on the reasoning for this behaviour over at the Cedlarland blog.

Solutions

Cedar has a validation engine that uses a schema to define the properties of entities within your system. This allows Cedar to warn you during the authoring phase when policies may not be valid. It is a best practice that you always construct a schema for your system.

The following schema would allow a developer to catch the unsafe usage of the attribute:

{
    "": {
        "entityTypes": {
            "Endpoint": {
                "shape": {
                    "type": "Record",
                    "attributes": {}
                }
            }
        },
        "actions": {
            "Connect": {
                "appliesTo": {
                    "resourceTypes": ["Endpoint"],
                    "context": {
                        "type": "Record",
                        "attributes": {
                            "viaAdminNetwork": { "type": "Boolean", "required": false }
                        }
                    }
                }
            }
        }
    }
}

Where possible, the inputs provided by the context object should be predictable. The developer may consider always setting the viaAdminNetwork key to simplify.

Alternatively, we can also modify the policy to test for the presence of the key itself, as shown:

permit(
    principal,
    action,
    resource
);

forbid(
    principal,
    action,
    resource
) unless {
    context has "viaAdminNetwork" && context.viaAdminNetwork == true
};

Developers might also consider overriding an allow result if any evaluation errors are present in the evaluation response, if that outcome is more desirable.

Dangers of short-circuiting

Short-circuiting is a performance feature of the Cedar language which allows it to skip evaluation of specific expressions that should not affect the result of the policy evaluation. It is present under the following conditions:

  • expression1 && expression2: expression2 is not evaluated when expression1 is false
  • expression1 || expression2: expression2 is not evaluated when expression1 is true
  • if expression1 then expression2 else expression3: expression2 is not evaluated when expression1 is false and expression3 is not evaluated when expression1 is true

This is typically a good thing, however it will not produce an error due to an invalid expression unless it actually evaluates that expression. For example, consider the below policy:

permit (
    principal,
    action == Action::"login",
    resource
)
when { context.isPrimarySite == true || principal.isBreakGlasEntity == true };

Note that this policy has the typo isBreakGlasEntity, which is missing an ‘s’. The intention behind the policy is that the login action is permitted only when accessing from the primary site under normal conditions, or if the principal is a special “break glass” entity under any conditions. This policy works under normal conditions, but due to the typo will error and not permit the break glass entity when they are most needed.

Solutions

A Cedar schema should again be used to determine the valid entity attributes during the entity modelling process and warn of inconsistencies during the policy authoring phase.

The following Cedar schema should be used to help find the typo during the authoring time of the policy:

{
    "": {
        "entityTypes": {
            "User": {
                "shape": {
                    "type": "Record",
                    "attributes": {
                        "isBreakGlassEntity": { "type": "Boolean", "required": true }
                    }
                }
            }
        },
        "actions": {
            "login": {
                "appliesTo": {
                    "principalTypes": [ "User" ],
                    "context": {
                        "type": "Record",
                        "attributes": {
                            "isPrimarySite": { "type": "Boolean", "required": true }
                        }
                    }
                }
            }
        }
    }
}

In addition to schema validation, it is also important to perform positive and negative testing against your policies (in a local or non-production environment) to ensure the policies will act in the way you expect for critical paths.

Ambiguous entity type

When writing condition statements which interact with an entity store, entities don’t have an inherit type associated with them. Consider the following entity store:

[
  {
    "uid": "User::\"alice\"",
    "attrs": {
      "active": true
    }
  },
  {
    "uid": "Action::\"redeemValidTicket\""
  },
  {
    "uid": "Ticket::\"someTicketID\"",
    "attrs": {
      "active": false
    }
  }
]

and the policy:

permit (
    principal,
    action == Action::"redeemValidTicket",
    resource
)
when { resource.active == true };

The intention behind this is to allow ticketholders redeem active tickets. The implementing developer allowed the full resource entity ID ("Ticket::\"someTicketID\"") be passed in as the resource input. Alice can’t redeem the "Ticket::\"someTicketID\"" resource as it is marked as not active, however Alice can perform a successful redemption with the resource entity ID "User::\"alice\"". Even though her user active attribute was never intended for that purpose, it nonetheless can lead to an unexpected allow.

Solutions

The developer could enforce that the “Ticket::” prefix is used (or perform the concatenation themselves).

The entity store could be modified to provide a unique attribute that the policy could match on using the has operator (resource has "ticketIssueDate").

The entity store could also be modified to place tickets in a new entity type “TicketGroup” using the parents construct and enforce via policy that the resource is within this group (resource in TicketGroup::"IssuedTickets").

Additionally, there is also a pending RFC that is discussing introducing an is operator to perform entity matching.

Unexpected order of operations

Like other languages, Cedar has a de-facto order of operations due to the way the grammar is constructed. This means that operations such as math works as you would expect:

permit (
    principal,
    action,
    resource
)
when { 1 + 2 * 3 + 4 * 5 == 27 }; // always true

It’s important to read and understand the grammar before constructing complex and ambiguous policies to avoid unintended effects. Consider the below policy:

permit (
    principal,
    action,
    resource
)
when {
    if resource.owner == principal then true else false &&
    resource.isRestricted == false
};

The intention behind the policy is to allow access when the principal is the resource owner and the resource is not restricted, however the effect of the policy is that a principal who is the resource owner is permitted access even when the resource is marked as restricted.

This is because the order of operations for an if-then-else operation is higher than that of the && operation and so the evaluation of the above condition is intrinsically like so:

if (resource.owner == principal) then (true) else (false && resource.isRestricted)

Solutions

Read the grammar when in doubt of the order of operations.

If you are ever in doubt, or simply want to be more explicit, use parentheses to explicitly show the intended grouping of operations:

permit (
    principal,
    action,
    resource
)
when {
    (if resource.owner == principal then true else false) &&
    resource.isRestricted == false
};

Side channels

Issues can often arise from the specific implementation that surrounds the use of Cedar, whether via Amazon Verified Permissions or a direct engine implementation. The engine can only evaluate against the inputs you have provided and if those inputs are not sanitized or invalid, it can lead to a compromise.

Late last year, the popular json5 library released a security advisory regarding the potential for prototype pollution. If you were to allow a user to specify their own context object, but override certain keys which were used in sensitive operations, an attacker could use this vulnerability to manipulate the inputs the Cedar engine receives.

// userInput = '{"foo": "bar", "__proto__": {"isAdmin": true}}'

const ctx = JSON5.parse(userInput);
if (secCheckKeysSet(ctx, ['isAdmin', 'isMod'])) {
  throw new Error('Forbidden...');
}

return avpclient.isAuthorized({
  'context': ctx,
  ...
});

Solutions

As always, a healthy supply-chain security program is recommended for organizations who make heavy use of external libraries. Input sanitization is also an important step to ensure that the engine can make appropriate authorization decisions.

As more and more built-in integrations become available, take advantage of these to shift more of the burden outside of your responsibility and avoid side-channel issues.

Wrapping up

As new language bindings, AWS integrations, external integrations, and even changes to the Cedar language itself continue to be produced, the overall community and ecosystem is growing. The scenarios above highlight the importance of a solid understanding of the language, but also solutions to help you overcome these hurdles and scale your authorization logic faster than would otherwise be possible.

If you liked what I’ve written, or want to hear more on this topic, reach out to me on Twitter at @iann0036. You can also join the discussion over at the official Cedar Slack workspace.